From f6b68142138935d156213a128a39bf9ae770d5de Mon Sep 17 00:00:00 2001 From: Newton Koumantzelis Date: Thu, 13 May 2021 10:57:50 -0700 Subject: [PATCH 1/5] feat: move to simpler, smaller idb-keyval lib to fix race condition --- docs/interest-groups.html | 43 +-- docs/scripts/frame/cjs/index.js | 473 +++++---------------------- docs/scripts/frame/esm/index.js | 473 +++++---------------------- docs/scripts/frame/iife/index.min.js | 2 +- package-lock.json | 8 +- package.json | 2 +- src/frame/api.js | 4 +- src/frame/auction/index.js | 6 +- src/frame/auction/utils.js | 6 +- src/frame/interest-groups/index.js | 15 +- src/frame/utils.js | 142 -------- test/e2e/index.html | 2 +- test/e2e/interest-groups.test.js | 4 +- test/unit/api/render.error.test.js | 48 --- 14 files changed, 220 insertions(+), 1008 deletions(-) delete mode 100644 src/frame/utils.js diff --git a/docs/interest-groups.html b/docs/interest-groups.html index ef31406..035e365 100644 --- a/docs/interest-groups.html +++ b/docs/interest-groups.html @@ -41,6 +41,7 @@

Currently Joined

diff --git a/docs/scripts/frame/cjs/index.js b/docs/scripts/frame/cjs/index.js index 93ed08f..06e2320 100644 --- a/docs/scripts/frame/cjs/index.js +++ b/docs/scripts/frame/cjs/index.js @@ -153,404 +153,108 @@ const echo = { const VERSION = 1; -const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c); - -let idbProxyableTypes; -let cursorAdvanceMethods; -// This is a function to prevent it throwing up in node environments. -function getIdbProxyableTypes() { - return (idbProxyableTypes || - (idbProxyableTypes = [ - IDBDatabase, - IDBObjectStore, - IDBIndex, - IDBCursor, - IDBTransaction, - ])); -} -// This is a function to prevent it throwing up in node environments. -function getCursorAdvanceMethods() { - return (cursorAdvanceMethods || - (cursorAdvanceMethods = [ - IDBCursor.prototype.advance, - IDBCursor.prototype.continue, - IDBCursor.prototype.continuePrimaryKey, - ])); -} -const cursorRequestMap = new WeakMap(); -const transactionDoneMap = new WeakMap(); -const transactionStoreNamesMap = new WeakMap(); -const transformCache = new WeakMap(); -const reverseTransformCache = new WeakMap(); function promisifyRequest(request) { - const promise = new Promise((resolve, reject) => { - const unlisten = () => { - request.removeEventListener('success', success); - request.removeEventListener('error', error); - }; - const success = () => { - resolve(wrap(request.result)); - unlisten(); - }; - const error = () => { - reject(request.error); - unlisten(); - }; - request.addEventListener('success', success); - request.addEventListener('error', error); + return new Promise((resolve, reject) => { + // @ts-ignore - file size hacks + request.oncomplete = request.onsuccess = () => resolve(request.result); + // @ts-ignore - file size hacks + request.onabort = request.onerror = () => reject(request.error); }); - promise - .then((value) => { - // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval - // (see wrapFunction). - if (value instanceof IDBCursor) { - cursorRequestMap.set(value, request); - } - // Catching to avoid "Uncaught Promise exceptions" - }) - .catch(() => { }); - // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This - // is because we create many promises from a single IDBRequest. - reverseTransformCache.set(promise, request); - return promise; -} -function cacheDonePromiseForTransaction(tx) { - // Early bail if we've already created a done promise for this transaction. - if (transactionDoneMap.has(tx)) - return; - const done = new Promise((resolve, reject) => { - const unlisten = () => { - tx.removeEventListener('complete', complete); - tx.removeEventListener('error', error); - tx.removeEventListener('abort', error); - }; - const complete = () => { - resolve(); - unlisten(); - }; - const error = () => { - reject(tx.error || new DOMException('AbortError', 'AbortError')); - unlisten(); - }; - tx.addEventListener('complete', complete); - tx.addEventListener('error', error); - tx.addEventListener('abort', error); - }); - // Cache it for later retrieval. - transactionDoneMap.set(tx, done); -} -let idbProxyTraps = { - get(target, prop, receiver) { - if (target instanceof IDBTransaction) { - // Special handling for transaction.done. - if (prop === 'done') - return transactionDoneMap.get(target); - // Polyfill for objectStoreNames because of Edge. - if (prop === 'objectStoreNames') { - return target.objectStoreNames || transactionStoreNamesMap.get(target); - } - // Make tx.store return the only store in the transaction, or undefined if there are many. - if (prop === 'store') { - return receiver.objectStoreNames[1] - ? undefined - : receiver.objectStore(receiver.objectStoreNames[0]); - } - } - // Else transform whatever we get back. - return wrap(target[prop]); - }, - set(target, prop, value) { - target[prop] = value; - return true; - }, - has(target, prop) { - if (target instanceof IDBTransaction && - (prop === 'done' || prop === 'store')) { - return true; - } - return prop in target; - }, -}; -function replaceTraps(callback) { - idbProxyTraps = callback(idbProxyTraps); } -function wrapFunction(func) { - // Due to expected object equality (which is enforced by the caching in `wrap`), we - // only create one new func per func. - // Edge doesn't support objectStoreNames (booo), so we polyfill it here. - if (func === IDBDatabase.prototype.transaction && - !('objectStoreNames' in IDBTransaction.prototype)) { - return function (storeNames, ...args) { - const tx = func.call(unwrap(this), storeNames, ...args); - transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]); - return wrap(tx); - }; - } - // Cursor methods are special, as the behaviour is a little more different to standard IDB. In - // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the - // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense - // with real promises, so each advance methods returns a new promise for the cursor object, or - // undefined if the end of the cursor has been reached. - if (getCursorAdvanceMethods().includes(func)) { - return function (...args) { - // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use - // the original object. - func.apply(unwrap(this), args); - return wrap(cursorRequestMap.get(this)); - }; - } - return function (...args) { - // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use - // the original object. - return wrap(func.apply(unwrap(this), args)); - }; -} -function transformCachableValue(value) { - if (typeof value === 'function') - return wrapFunction(value); - // This doesn't return, it just creates a 'done' promise for the transaction, - // which is later returned for transaction.done (see idbObjectHandler). - if (value instanceof IDBTransaction) - cacheDonePromiseForTransaction(value); - if (instanceOfAny(value, getIdbProxyableTypes())) - return new Proxy(value, idbProxyTraps); - // Return the same value back if we're not going to transform it. - return value; +function createStore(dbName, storeName) { + const request = indexedDB.open(dbName); + request.onupgradeneeded = () => request.result.createObjectStore(storeName); + const dbp = promisifyRequest(request); + return (txMode, callback) => dbp.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); } -function wrap(value) { - // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because - // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. - if (value instanceof IDBRequest) - return promisifyRequest(value); - // If we've already transformed this value before, reuse the transformed value. - // This is faster, but it also provides object equality. - if (transformCache.has(value)) - return transformCache.get(value); - const newValue = transformCachableValue(value); - // Not all types are transformed. - // These may be primitive types, so they can't be WeakMap keys. - if (newValue !== value) { - transformCache.set(value, newValue); - reverseTransformCache.set(newValue, value); +let defaultGetStoreFunc; +function defaultGetStore() { + if (!defaultGetStoreFunc) { + defaultGetStoreFunc = createStore('keyval-store', 'keyval'); } - return newValue; + return defaultGetStoreFunc; } -const unwrap = (value) => reverseTransformCache.get(value); - /** - * Open a database. + * Get a value by its key. * - * @param name Name of the database. - * @param version Schema version. - * @param callbacks Additional callbacks. + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) { - const request = indexedDB.open(name, version); - const openPromise = wrap(request); - if (upgrade) { - request.addEventListener('upgradeneeded', (event) => { - upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction)); - }); - } - if (blocked) - request.addEventListener('blocked', () => blocked()); - openPromise - .then((db) => { - if (terminated) - db.addEventListener('close', () => terminated()); - if (blocking) - db.addEventListener('versionchange', () => blocking()); - }) - .catch(() => { }); - return openPromise; +function get(key, customStore = defaultGetStore()) { + return customStore('readonly', (store) => promisifyRequest(store.get(key))); } - -const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count']; -const writeMethods = ['put', 'add', 'delete', 'clear']; -const cachedMethods = new Map(); -function getMethod(target, prop) { - if (!(target instanceof IDBDatabase && - !(prop in target) && - typeof prop === 'string')) { - return; - } - if (cachedMethods.get(prop)) - return cachedMethods.get(prop); - const targetFuncName = prop.replace(/FromIndex$/, ''); - const useIndex = prop !== targetFuncName; - const isWrite = writeMethods.includes(targetFuncName); - if ( - // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. - !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || - !(isWrite || readMethods.includes(targetFuncName))) { - return; - } - const method = async function (storeName, ...args) { - // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( - const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly'); - let target = tx.store; - if (useIndex) - target = target.index(args.shift()); - // Must reject if op rejects. - // If it's a write operation, must reject if tx.done rejects. - // Must reject with op rejection first. - // Must resolve with op value. - // Must handle both promises (no unhandled rejections) - return (await Promise.all([ - target[targetFuncName](...args), - isWrite && tx.done, - ]))[0]; - }; - cachedMethods.set(prop, method); - return method; -} -replaceTraps((oldTraps) => ({ - ...oldTraps, - get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), - has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop), -})); - -/* - * @const {string} - * @summary the name of the Interest Group store within IndexedDB - */ -const IG_STORE = 'interest-groups'; - -/* - * @function - * @name db - * @description create an Indexed dB - * @author Newton - * @return {promise} a promise - */ -const db = openDB('Fledge', 1, { - upgrade (db) { - // Create a store of objects - const igStore = db.createObjectStore(IG_STORE, { - // The '_key' property of the object will be the key. - keyPath: '_key', - }); - - // Create an index on the a few properties of the objects. - [ 'owner', 'name', '_expired' ].forEach(index => { - igStore.createIndex(index, index, { unique: false }); - }); - }, -}); - -/* - * @function - * @name getItemFromStore - * @description retrieve an item from an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {string} id - the id; typically matches the keyPath of a store - * @return {object} an object representing an interest group +/** + * Set a value with a key. * - * @example - * store.get('someStore', 'foo'); + * @param key + * @param value + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function getItemFromStore (store, id) { - const item = (await db).get(store, id); - - if (item) { - return item; - } - - return null; +function set(key, value, customStore = defaultGetStore()) { + return customStore('readwrite', (store) => { + store.put(value, key); + return promisifyRequest(store.transaction); + }); } - -/* - * @function - * @name getAllFromStore - * @description retrieve all items from an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @return {array} an array of objects representing all items from a store +/** + * Update a value. This lets you see the old value and update it as an atomic operation. * - * @example - * store.getAll('someStore'); + * @param key + * @param updater A callback that takes the old value and returns a new value. + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function getAllFromStore (store) { - const items = (await db).getAll(store); - - if (items) { - return items; - } - - return null; +function update(key, updater, customStore = defaultGetStore()) { + return customStore('readwrite', (store) => + // Need to create the promise manually. + // If I try to chain promises, the transaction closes in browsers + // that use a promise polyfill (IE10/11). + new Promise((resolve, reject) => { + store.get(key).onsuccess = function () { + try { + store.put(updater(this.result), key); + resolve(promisifyRequest(store.transaction)); + } + catch (err) { + reject(err); + } + }; + })); } - -/* - * @function - * @name updateItemInStore - * @description update an item in an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {object} item - An existing item - * @param {object} newOptions - a new set of options to merge with the item - * @return {string} the key of the item updated +/** + * Delete a particular key from the store. * - * @example - * store.put('someStore', { bidding_logic_url: '://v2/bid' }, { owner: 'foo', name: 'bar' }, 1234); + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function updateItemInStore (store, item, newOptions) { - const updated = { - ...item, - ...newOptions, - _updated: Date.now(), - }; - - return (await db).put(store, updated); +function del(key, customStore = defaultGetStore()) { + return customStore('readwrite', (store) => { + store.delete(key); + return promisifyRequest(store.transaction); + }); } - -/* - * @function - * @name createItemInStore - * @description create an item in an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {object} options - An object of options to make up item - * @return {string} the key of the item created - * - * @example - * store.add('someStore', { owner: 'foo', name: 'bar' }); - */ -async function createItemInStore (store, options) { - return (await db).add(store, { - ...options, - _created: Date.now(), - _updated: Date.now(), - }); +function eachCursor(customStore, callback) { + return customStore('readonly', (store) => { + // This would be store.getAllKeys(), but it isn't supported by Edge or Safari. + // And openKeyCursor isn't supported by Safari. + store.openCursor().onsuccess = function () { + if (!this.result) + return; + callback(this.result); + this.result.continue(); + }; + return promisifyRequest(store.transaction); + }); } - -/* - * @function - * @name deleteItemFromStore - * @description delete a record from Indexed dB - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {string} id - the id; typically matches the keyPath of a store - * @return {undefined} +/** + * Get all entries in the store. Each entry is an array of `[key, value]`. * - * @example - * store.delete('owner-name'); + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function deleteItemFromStore (store, id) { - return (await db).delete(store, id); +function entries(customStore = defaultGetStore()) { + const items = []; + return eachCursor(customStore, (cursor) => items.push([cursor.key, cursor.value])).then(() => items); } -var db$1 = { - db, - store: { - add: createItemInStore, - get: getItemFromStore, - getAll: getAllFromStore, - put: updateItemInStore, - delete: deleteItemFromStore, - }, -}; - /* eslint-disable camelcase */ /* @@ -570,7 +274,7 @@ const getEligible = (groups, eligibility, debug) => { return groups; } - const eligible = groups.filter(({ owner }) => eligibility.includes(owner)); + const eligible = groups.filter(([ key, value ]) => eligibility.includes(value.owner)); if (eligible.length) { debug && echo.info(`found some eligible buyers`); debug && echo.groupEnd(); @@ -592,8 +296,8 @@ const getEligible = (groups, eligibility, debug) => { * @return {object | null} an array of objects containing bids; null if none found */ const getBids = async (bidders, conf, debug) => Promise.all( - bidders.map(async bidder => { - debug && echo.groupCollapsed(`auction utils: getBids => ${bidder._key}`); + bidders.map(async ([ key, bidder ]) => { + debug && echo.groupCollapsed(`auction utils: getBids => ${key}`); const time0 = performance.now(); const { generateBid, generate_bid } = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(bidder.bidding_logic_url)); }); let callBid = generateBid; @@ -781,7 +485,7 @@ const getTrustedSignals = async (url, keys, debug) => { */ async function runAdAuction (conf, debug) { debug && echo.groupCollapsed('Fledge API: runAdAuction'); - const interestGroups = await db$1.store.getAll(IG_STORE); + const interestGroups = await entries(); debug && echo.log(echo.asInfo('all interest groups:'), interestGroups); const eligible = getEligible(interestGroups, conf.interest_group_buyers, debug); @@ -855,20 +559,21 @@ const getIGKey = (owner, name) => `${owner}-${name}`; */ async function joinAdInterestGroup (options, expiry, debug) { debug && echo.groupCollapsed('Fledge API: joinAdInterest'); - const group = await db$1.store.get(IG_STORE, getIGKey(options.owner, options.name)); + const id = getIGKey(options.owner, options.name); + const group = await get(id); debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); - let id; if (group) { debug && echo.log(echo.asProcess('updating an interest group')); - id = await db$1.store.put(IG_STORE, group, { + await update(id, { _expired: Date.now() + expiry, ...options, }); } else { debug && echo.log(echo.asProcess('creating a new interest group')); - id = await db$1.store.add(IG_STORE, { - _key: getIGKey(options.owner, options.name), + await set(id, { + _created: Date.now(), _expired: Date.now() + expiry, + _updated: Date.now(), ...options, }); } @@ -893,7 +598,7 @@ async function joinAdInterestGroup (options, expiry, debug) { async function leaveAdInterestGroup (group, debug) { debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); debug && echo.log(echo.asProcess('deleting an existing interest group')); - await db$1.store.delete(IG_STORE, getIGKey(group.owner, group.name)); + await del(getIGKey(group.owner, group.name)); debug && echo.log(echo.asSuccess('interest group deleted')); debug && echo.groupEnd(); diff --git a/docs/scripts/frame/esm/index.js b/docs/scripts/frame/esm/index.js index b27c976..a005086 100644 --- a/docs/scripts/frame/esm/index.js +++ b/docs/scripts/frame/esm/index.js @@ -131,404 +131,108 @@ const echo = { const VERSION = 1; -const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c); - -let idbProxyableTypes; -let cursorAdvanceMethods; -// This is a function to prevent it throwing up in node environments. -function getIdbProxyableTypes() { - return (idbProxyableTypes || - (idbProxyableTypes = [ - IDBDatabase, - IDBObjectStore, - IDBIndex, - IDBCursor, - IDBTransaction, - ])); -} -// This is a function to prevent it throwing up in node environments. -function getCursorAdvanceMethods() { - return (cursorAdvanceMethods || - (cursorAdvanceMethods = [ - IDBCursor.prototype.advance, - IDBCursor.prototype.continue, - IDBCursor.prototype.continuePrimaryKey, - ])); -} -const cursorRequestMap = new WeakMap(); -const transactionDoneMap = new WeakMap(); -const transactionStoreNamesMap = new WeakMap(); -const transformCache = new WeakMap(); -const reverseTransformCache = new WeakMap(); function promisifyRequest(request) { - const promise = new Promise((resolve, reject) => { - const unlisten = () => { - request.removeEventListener('success', success); - request.removeEventListener('error', error); - }; - const success = () => { - resolve(wrap(request.result)); - unlisten(); - }; - const error = () => { - reject(request.error); - unlisten(); - }; - request.addEventListener('success', success); - request.addEventListener('error', error); + return new Promise((resolve, reject) => { + // @ts-ignore - file size hacks + request.oncomplete = request.onsuccess = () => resolve(request.result); + // @ts-ignore - file size hacks + request.onabort = request.onerror = () => reject(request.error); }); - promise - .then((value) => { - // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval - // (see wrapFunction). - if (value instanceof IDBCursor) { - cursorRequestMap.set(value, request); - } - // Catching to avoid "Uncaught Promise exceptions" - }) - .catch(() => { }); - // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This - // is because we create many promises from a single IDBRequest. - reverseTransformCache.set(promise, request); - return promise; -} -function cacheDonePromiseForTransaction(tx) { - // Early bail if we've already created a done promise for this transaction. - if (transactionDoneMap.has(tx)) - return; - const done = new Promise((resolve, reject) => { - const unlisten = () => { - tx.removeEventListener('complete', complete); - tx.removeEventListener('error', error); - tx.removeEventListener('abort', error); - }; - const complete = () => { - resolve(); - unlisten(); - }; - const error = () => { - reject(tx.error || new DOMException('AbortError', 'AbortError')); - unlisten(); - }; - tx.addEventListener('complete', complete); - tx.addEventListener('error', error); - tx.addEventListener('abort', error); - }); - // Cache it for later retrieval. - transactionDoneMap.set(tx, done); -} -let idbProxyTraps = { - get(target, prop, receiver) { - if (target instanceof IDBTransaction) { - // Special handling for transaction.done. - if (prop === 'done') - return transactionDoneMap.get(target); - // Polyfill for objectStoreNames because of Edge. - if (prop === 'objectStoreNames') { - return target.objectStoreNames || transactionStoreNamesMap.get(target); - } - // Make tx.store return the only store in the transaction, or undefined if there are many. - if (prop === 'store') { - return receiver.objectStoreNames[1] - ? undefined - : receiver.objectStore(receiver.objectStoreNames[0]); - } - } - // Else transform whatever we get back. - return wrap(target[prop]); - }, - set(target, prop, value) { - target[prop] = value; - return true; - }, - has(target, prop) { - if (target instanceof IDBTransaction && - (prop === 'done' || prop === 'store')) { - return true; - } - return prop in target; - }, -}; -function replaceTraps(callback) { - idbProxyTraps = callback(idbProxyTraps); } -function wrapFunction(func) { - // Due to expected object equality (which is enforced by the caching in `wrap`), we - // only create one new func per func. - // Edge doesn't support objectStoreNames (booo), so we polyfill it here. - if (func === IDBDatabase.prototype.transaction && - !('objectStoreNames' in IDBTransaction.prototype)) { - return function (storeNames, ...args) { - const tx = func.call(unwrap(this), storeNames, ...args); - transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]); - return wrap(tx); - }; - } - // Cursor methods are special, as the behaviour is a little more different to standard IDB. In - // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the - // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense - // with real promises, so each advance methods returns a new promise for the cursor object, or - // undefined if the end of the cursor has been reached. - if (getCursorAdvanceMethods().includes(func)) { - return function (...args) { - // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use - // the original object. - func.apply(unwrap(this), args); - return wrap(cursorRequestMap.get(this)); - }; - } - return function (...args) { - // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use - // the original object. - return wrap(func.apply(unwrap(this), args)); - }; -} -function transformCachableValue(value) { - if (typeof value === 'function') - return wrapFunction(value); - // This doesn't return, it just creates a 'done' promise for the transaction, - // which is later returned for transaction.done (see idbObjectHandler). - if (value instanceof IDBTransaction) - cacheDonePromiseForTransaction(value); - if (instanceOfAny(value, getIdbProxyableTypes())) - return new Proxy(value, idbProxyTraps); - // Return the same value back if we're not going to transform it. - return value; +function createStore(dbName, storeName) { + const request = indexedDB.open(dbName); + request.onupgradeneeded = () => request.result.createObjectStore(storeName); + const dbp = promisifyRequest(request); + return (txMode, callback) => dbp.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); } -function wrap(value) { - // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because - // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. - if (value instanceof IDBRequest) - return promisifyRequest(value); - // If we've already transformed this value before, reuse the transformed value. - // This is faster, but it also provides object equality. - if (transformCache.has(value)) - return transformCache.get(value); - const newValue = transformCachableValue(value); - // Not all types are transformed. - // These may be primitive types, so they can't be WeakMap keys. - if (newValue !== value) { - transformCache.set(value, newValue); - reverseTransformCache.set(newValue, value); +let defaultGetStoreFunc; +function defaultGetStore() { + if (!defaultGetStoreFunc) { + defaultGetStoreFunc = createStore('keyval-store', 'keyval'); } - return newValue; + return defaultGetStoreFunc; } -const unwrap = (value) => reverseTransformCache.get(value); - /** - * Open a database. + * Get a value by its key. * - * @param name Name of the database. - * @param version Schema version. - * @param callbacks Additional callbacks. + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) { - const request = indexedDB.open(name, version); - const openPromise = wrap(request); - if (upgrade) { - request.addEventListener('upgradeneeded', (event) => { - upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction)); - }); - } - if (blocked) - request.addEventListener('blocked', () => blocked()); - openPromise - .then((db) => { - if (terminated) - db.addEventListener('close', () => terminated()); - if (blocking) - db.addEventListener('versionchange', () => blocking()); - }) - .catch(() => { }); - return openPromise; +function get(key, customStore = defaultGetStore()) { + return customStore('readonly', (store) => promisifyRequest(store.get(key))); } - -const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count']; -const writeMethods = ['put', 'add', 'delete', 'clear']; -const cachedMethods = new Map(); -function getMethod(target, prop) { - if (!(target instanceof IDBDatabase && - !(prop in target) && - typeof prop === 'string')) { - return; - } - if (cachedMethods.get(prop)) - return cachedMethods.get(prop); - const targetFuncName = prop.replace(/FromIndex$/, ''); - const useIndex = prop !== targetFuncName; - const isWrite = writeMethods.includes(targetFuncName); - if ( - // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. - !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || - !(isWrite || readMethods.includes(targetFuncName))) { - return; - } - const method = async function (storeName, ...args) { - // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( - const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly'); - let target = tx.store; - if (useIndex) - target = target.index(args.shift()); - // Must reject if op rejects. - // If it's a write operation, must reject if tx.done rejects. - // Must reject with op rejection first. - // Must resolve with op value. - // Must handle both promises (no unhandled rejections) - return (await Promise.all([ - target[targetFuncName](...args), - isWrite && tx.done, - ]))[0]; - }; - cachedMethods.set(prop, method); - return method; -} -replaceTraps((oldTraps) => ({ - ...oldTraps, - get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), - has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop), -})); - -/* - * @const {string} - * @summary the name of the Interest Group store within IndexedDB - */ -const IG_STORE = 'interest-groups'; - -/* - * @function - * @name db - * @description create an Indexed dB - * @author Newton - * @return {promise} a promise - */ -const db = openDB('Fledge', 1, { - upgrade (db) { - // Create a store of objects - const igStore = db.createObjectStore(IG_STORE, { - // The '_key' property of the object will be the key. - keyPath: '_key', - }); - - // Create an index on the a few properties of the objects. - [ 'owner', 'name', '_expired' ].forEach(index => { - igStore.createIndex(index, index, { unique: false }); - }); - }, -}); - -/* - * @function - * @name getItemFromStore - * @description retrieve an item from an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {string} id - the id; typically matches the keyPath of a store - * @return {object} an object representing an interest group +/** + * Set a value with a key. * - * @example - * store.get('someStore', 'foo'); + * @param key + * @param value + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function getItemFromStore (store, id) { - const item = (await db).get(store, id); - - if (item) { - return item; - } - - return null; +function set(key, value, customStore = defaultGetStore()) { + return customStore('readwrite', (store) => { + store.put(value, key); + return promisifyRequest(store.transaction); + }); } - -/* - * @function - * @name getAllFromStore - * @description retrieve all items from an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @return {array} an array of objects representing all items from a store +/** + * Update a value. This lets you see the old value and update it as an atomic operation. * - * @example - * store.getAll('someStore'); + * @param key + * @param updater A callback that takes the old value and returns a new value. + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function getAllFromStore (store) { - const items = (await db).getAll(store); - - if (items) { - return items; - } - - return null; +function update(key, updater, customStore = defaultGetStore()) { + return customStore('readwrite', (store) => + // Need to create the promise manually. + // If I try to chain promises, the transaction closes in browsers + // that use a promise polyfill (IE10/11). + new Promise((resolve, reject) => { + store.get(key).onsuccess = function () { + try { + store.put(updater(this.result), key); + resolve(promisifyRequest(store.transaction)); + } + catch (err) { + reject(err); + } + }; + })); } - -/* - * @function - * @name updateItemInStore - * @description update an item in an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {object} item - An existing item - * @param {object} newOptions - a new set of options to merge with the item - * @return {string} the key of the item updated +/** + * Delete a particular key from the store. * - * @example - * store.put('someStore', { bidding_logic_url: '://v2/bid' }, { owner: 'foo', name: 'bar' }, 1234); + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function updateItemInStore (store, item, newOptions) { - const updated = { - ...item, - ...newOptions, - _updated: Date.now(), - }; - - return (await db).put(store, updated); +function del(key, customStore = defaultGetStore()) { + return customStore('readwrite', (store) => { + store.delete(key); + return promisifyRequest(store.transaction); + }); } - -/* - * @function - * @name createItemInStore - * @description create an item in an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {object} options - An object of options to make up item - * @return {string} the key of the item created - * - * @example - * store.add('someStore', { owner: 'foo', name: 'bar' }); - */ -async function createItemInStore (store, options) { - return (await db).add(store, { - ...options, - _created: Date.now(), - _updated: Date.now(), - }); +function eachCursor(customStore, callback) { + return customStore('readonly', (store) => { + // This would be store.getAllKeys(), but it isn't supported by Edge or Safari. + // And openKeyCursor isn't supported by Safari. + store.openCursor().onsuccess = function () { + if (!this.result) + return; + callback(this.result); + this.result.continue(); + }; + return promisifyRequest(store.transaction); + }); } - -/* - * @function - * @name deleteItemFromStore - * @description delete a record from Indexed dB - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {string} id - the id; typically matches the keyPath of a store - * @return {undefined} +/** + * Get all entries in the store. Each entry is an array of `[key, value]`. * - * @example - * store.delete('owner-name'); + * @param customStore Method to get a custom store. Use with caution (see the docs). */ -async function deleteItemFromStore (store, id) { - return (await db).delete(store, id); +function entries(customStore = defaultGetStore()) { + const items = []; + return eachCursor(customStore, (cursor) => items.push([cursor.key, cursor.value])).then(() => items); } -var db$1 = { - db, - store: { - add: createItemInStore, - get: getItemFromStore, - getAll: getAllFromStore, - put: updateItemInStore, - delete: deleteItemFromStore, - }, -}; - /* eslint-disable camelcase */ /* @@ -548,7 +252,7 @@ const getEligible = (groups, eligibility, debug) => { return groups; } - const eligible = groups.filter(({ owner }) => eligibility.includes(owner)); + const eligible = groups.filter(([ key, value ]) => eligibility.includes(value.owner)); if (eligible.length) { debug && echo.info(`found some eligible buyers`); debug && echo.groupEnd(); @@ -570,8 +274,8 @@ const getEligible = (groups, eligibility, debug) => { * @return {object | null} an array of objects containing bids; null if none found */ const getBids = async (bidders, conf, debug) => Promise.all( - bidders.map(async bidder => { - debug && echo.groupCollapsed(`auction utils: getBids => ${bidder._key}`); + bidders.map(async ([ key, bidder ]) => { + debug && echo.groupCollapsed(`auction utils: getBids => ${key}`); const time0 = performance.now(); const { generateBid, generate_bid } = await import(bidder.bidding_logic_url); let callBid = generateBid; @@ -759,7 +463,7 @@ const getTrustedSignals = async (url, keys, debug) => { */ async function runAdAuction (conf, debug) { debug && echo.groupCollapsed('Fledge API: runAdAuction'); - const interestGroups = await db$1.store.getAll(IG_STORE); + const interestGroups = await entries(); debug && echo.log(echo.asInfo('all interest groups:'), interestGroups); const eligible = getEligible(interestGroups, conf.interest_group_buyers, debug); @@ -833,20 +537,21 @@ const getIGKey = (owner, name) => `${owner}-${name}`; */ async function joinAdInterestGroup (options, expiry, debug) { debug && echo.groupCollapsed('Fledge API: joinAdInterest'); - const group = await db$1.store.get(IG_STORE, getIGKey(options.owner, options.name)); + const id = getIGKey(options.owner, options.name); + const group = await get(id); debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); - let id; if (group) { debug && echo.log(echo.asProcess('updating an interest group')); - id = await db$1.store.put(IG_STORE, group, { + await update(id, { _expired: Date.now() + expiry, ...options, }); } else { debug && echo.log(echo.asProcess('creating a new interest group')); - id = await db$1.store.add(IG_STORE, { - _key: getIGKey(options.owner, options.name), + await set(id, { + _created: Date.now(), _expired: Date.now() + expiry, + _updated: Date.now(), ...options, }); } @@ -871,7 +576,7 @@ async function joinAdInterestGroup (options, expiry, debug) { async function leaveAdInterestGroup (group, debug) { debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); debug && echo.log(echo.asProcess('deleting an existing interest group')); - await db$1.store.delete(IG_STORE, getIGKey(group.owner, group.name)); + await del(getIGKey(group.owner, group.name)); debug && echo.log(echo.asSuccess('interest group deleted')); debug && echo.groupEnd(); diff --git a/docs/scripts/frame/iife/index.min.js b/docs/scripts/frame/iife/index.min.js index 954541e..f428d5c 100644 --- a/docs/scripts/frame/iife/index.min.js +++ b/docs/scripts/frame/iife/index.min.js @@ -1 +1 @@ -var fledgeframe=function(){"use strict";let e=[];const n={},t="%c ";function o(o){return function(...r){const s=[],a=[];r.forEach((o=>{if(o===n){const n=e.shift();s.push(`%c${n.value}`,t),a.push(n.css,"")}else"object"==typeof o||"function"==typeof o?(s.push("%o",t),a.push(o,"")):(s.push(`%c${o}`,t),a.push("",""))})),o(s.join(""),...a),e=[]}}const r={assert:o(console.assert),clear:o(console.clear),count:o(console.count),countReset:o(console.countReset),debug:o(console.debug),dir:o(console.dir),error:o(console.error),group:o(console.group),groupCollapsed:o(console.groupCollapsed),groupEnd:o(console.groupEnd),info:o(console.info),log:o(console.log),table:o(console.table),time:o(console.time),timeEnd:o(console.timeEnd),timeLog:o(console.timeLog),trace:o(console.trace),warn:o(console.warn),asAlert:function(t){return e.push({value:t,css:"display: inline-block; background-color: #dc3545; color: #ffffff; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),n},asInfo:function(t){return e.push({value:t,css:"color: #0366d6; font-weight: bold;"}),n},asProcess:function(t){return e.push({value:`${t}…`,css:"color: #8c8c8c; font-style: italic;"}),n},asSuccess:function(t){return e.push({value:t,css:"color: #289d45; font-weight: bold;"}),n},asWarning:function(t){return e.push({value:t,css:"display: inline-block; background-color: #ffc107; color: black; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),n}};let s,a;const i=new WeakMap,c=new WeakMap,l=new WeakMap,u=new WeakMap,d=new WeakMap;let g={get(e,n,t){if(e instanceof IDBTransaction){if("done"===n)return c.get(e);if("objectStoreNames"===n)return e.objectStoreNames||l.get(e);if("store"===n)return t.objectStoreNames[1]?void 0:t.objectStore(t.objectStoreNames[0])}return w(e[n])},set:(e,n,t)=>(e[n]=t,!0),has:(e,n)=>e instanceof IDBTransaction&&("done"===n||"store"===n)||n in e};function p(e){return e!==IDBDatabase.prototype.transaction||"objectStoreNames"in IDBTransaction.prototype?(a||(a=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(e)?function(...n){return e.apply(h(this),n),w(i.get(this))}:function(...n){return w(e.apply(h(this),n))}:function(n,...t){const o=e.call(h(this),n,...t);return l.set(o,n.sort?n.sort():[n]),w(o)}}function f(e){return"function"==typeof e?p(e):(e instanceof IDBTransaction&&function(e){if(c.has(e))return;const n=new Promise(((n,t)=>{const o=()=>{e.removeEventListener("complete",r),e.removeEventListener("error",s),e.removeEventListener("abort",s)},r=()=>{n(),o()},s=()=>{t(e.error||new DOMException("AbortError","AbortError")),o()};e.addEventListener("complete",r),e.addEventListener("error",s),e.addEventListener("abort",s)}));c.set(e,n)}(e),n=e,(s||(s=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])).some((e=>n instanceof e))?new Proxy(e,g):e);var n}function w(e){if(e instanceof IDBRequest)return function(e){const n=new Promise(((n,t)=>{const o=()=>{e.removeEventListener("success",r),e.removeEventListener("error",s)},r=()=>{n(w(e.result)),o()},s=()=>{t(e.error),o()};e.addEventListener("success",r),e.addEventListener("error",s)}));return n.then((n=>{n instanceof IDBCursor&&i.set(n,e)})).catch((()=>{})),d.set(n,e),n}(e);if(u.has(e))return u.get(e);const n=f(e);return n!==e&&(u.set(e,n),d.set(n,e)),n}const h=e=>d.get(e);const y=["get","getKey","getAll","getAllKeys","count"],b=["put","add","delete","clear"],m=new Map;function E(e,n){if(!(e instanceof IDBDatabase)||n in e||"string"!=typeof n)return;if(m.get(n))return m.get(n);const t=n.replace(/FromIndex$/,""),o=n!==t,r=b.includes(t);if(!(t in(o?IDBIndex:IDBObjectStore).prototype)||!r&&!y.includes(t))return;const s=async function(e,...n){const s=this.transaction(e,r?"readwrite":"readonly");let a=s.store;return o&&(a=a.index(n.shift())),(await Promise.all([a[t](...n),r&&s.done]))[0]};return m.set(n,s),s}g=(e=>({...e,get:(n,t,o)=>E(n,t)||e.get(n,t,o),has:(n,t)=>!!E(n,t)||e.has(n,t)}))(g);const I="interest-groups",_=function(e,n,{blocked:t,upgrade:o,blocking:r,terminated:s}={}){const a=indexedDB.open(e,n),i=w(a);return o&&a.addEventListener("upgradeneeded",(e=>{o(w(a.result),e.oldVersion,e.newVersion,w(a.transaction))})),t&&a.addEventListener("blocked",(()=>t())),i.then((e=>{s&&e.addEventListener("close",(()=>s())),r&&e.addEventListener("versionchange",(()=>r()))})).catch((()=>{})),i}("Fledge",1,{upgrade(e){const n=e.createObjectStore(I,{keyPath:"_key"});["owner","name","_expired"].forEach((e=>{n.createIndex(e,e,{unique:!1})}))}});var v={db:_,store:{add:async function(e,n){return(await _).add(e,{...n,_created:Date.now(),_updated:Date.now()})},get:async function(e,n){const t=(await _).get(e,n);return t||null},getAll:async function(e){const n=(await _).getAll(e);return n||null},put:async function(e,n,t){const o={...n,...t,_updated:Date.now()};return(await _).put(e,o)},delete:async function(e,n){return(await _).delete(e,n)}}};const A=async(e,n,t)=>{t&&r.groupCollapsed("auction utils: getTrustedSignals");const o=`hostname=${window.top.location.hostname}`;if(!e||!n)return t&&r.log(r.asWarning("No 'url' or 'keys' found!")),void(t&&r.groupEnd());const s=await fetch(`${e}?${o}&keys=${n.join(",")}`).then((e=>{if(!e.ok)throw new Error("Something went wrong! The response returned was not ok.");if(!(e=>/\bapplication\/json\b/.test(e?.headers?.get("content-type")))(e))throw new Error("Response was not in the format of JSON.");return e.json()})).catch((e=>(t&&r.log(r.asAlert("There was a problem with your fetch operation:")),t&&r.log(e),null))),a={};for(const[e,t]of s)n.includes(e)&&(a[e]=t);return a&&0===Object.keys(a).length&&a.constructor===Object?(t&&r.groupEnd(),a):(t&&r.log(r.asWarning("No signals found!")),t&&r.groupEnd(),null)};async function x(e,n){n&&r.groupCollapsed("Fledge API: runAdAuction");const t=await v.store.getAll(I);n&&r.log(r.asInfo("all interest groups:"),t);const o=((e,n,t)=>{if(t&&r.groupCollapsed("auction utils: getEligible"),"*"===n)return t&&r.info("using the wildcard yields all groups"),t&&r.groupEnd(),e;const o=e.filter((({owner:e})=>n.includes(e)));return o.length?(t&&r.info("found some eligible buyers"),t&&r.groupEnd(),o):(t&&r.log(r.asWarning("No groups were eligible!")),t&&r.groupEnd(),null)})(t,e.interest_group_buyers,n);if(n&&r.log(r.asInfo('eligible buyers based on "interest_group_buyers":'),o),!o)return n&&r.log(r.asAlert("No eligible interest group buyers found!")),null;const s=await(async(e,n,t)=>Promise.all(e.map((async e=>{t&&r.groupCollapsed(`auction utils: getBids => ${e._key}`);const o=performance.now(),{generateBid:s,generate_bid:a}=await import(e.bidding_logic_url);let i=s;if(a&&!s&&(i=a),!i&&"function"!=typeof i)return t&&r.log(r.asWarning("No 'generateBid' function found!")),t&&r.groupEnd(),null;const c=await A(e?.trusted_bidding_signals_url,e?.trusted_bidding_signals_keys,t);let l;try{l=i(e,n?.auction_signals,n?.per_buyer_signals?.[e.owner],c,{top_window_hostname:window.top.location.hostname,seller:n.seller}),t&&r.log(r.asInfo("bid:"),l)}catch(e){return t&&r.log(r.asAlert("There was an error in the 'generateBid' function:")),t&&r.log(e),null}if(!(l.ad&&"object"==typeof l.ad&&l.bid&&"number"==typeof l.bid&&l.render)||"string"!=typeof l.render&&!Array.isArray(l.render))return t&&r.log(r.asWarning("No bid found!")),t&&r.groupEnd(),null;const u=performance.now();return t&&r.groupEnd(),{...e,...l,duration:u-o}}))))(o,e,n);n&&r.log(r.asInfo("all bids from each buyer:"),s);const a=s.filter((e=>e));if(n&&r.log(r.asInfo("filtered bids:"),a),!a.length)return n&&r.log(r.asAlert("No bids found!")),n&&r.groupEnd(),null;n&&r.log(r.asProcess("getting all scores, filtering and sorting"));const[i]=await(async(e,n,t)=>{t&&r.groupCollapsed("auction utils: getScores");const{scoreAd:o,score_ad:s}=await import(n.decision_logic_url);let a=o;return s&&!o&&(a=s),a||"function"==typeof a?e.map((e=>{let o;try{o=a(e?.ad,e?.bid,n,n?.trusted_scoring_signals,{top_window_hostname:window.top.location.hostname,interest_group_owner:e.owner,interest_group_name:e.name,bidding_duration_msec:e.duration}),t&&r.log(r.asInfo("score:"),o)}catch(e){t&&r.log(r.asAlert("There was an error in the 'scoreAd' function:")),t&&r.log(e),o=-1}return t&&r.groupEnd(),{bid:e,score:o}})).filter((({score:e})=>e>0)).sort(((e,n)=>e.score>n.score?1:-1)):(t&&r.log(r.asWarning("No 'scoreAd' function was found!")),null)})(a,e,n);if(n&&r.log(r.asInfo("winner:"),i),!i)return n&&r.log(r.asAlert("No winner found!")),n&&r.groupEnd(),null;n&&r.log(r.asProcess("creating an entry in the auction store"));const c=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(e=>(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)));return sessionStorage.setItem(c,JSON.stringify({origin:`${window.top.location.origin}${window.top.location.pathname}`,timestamp:Date.now(),conf:e,...i})),n&&r.log(r.asSuccess("auction token:"),c),n&&r.groupEnd(),c}const D=(e,n)=>`${e}-${n}`;async function k({data:e,ports:n}){try{if(!Array.isArray(e))throw new Error(`The API expects the data to be in the form of an array, with index 0 set to the action, and index 1 set to the data. A ${typeof e} was passed instead.`);switch(e[0]){case"joinAdInterestGroup":{const[,n]=e,[t,o,s]=n;return await async function(e,n,t){t&&r.groupCollapsed("Fledge API: joinAdInterest");const o=await v.store.get(I,D(e.owner,e.name));let s;return t&&r.log(r.asInfo("checking for an existing interest group:"),o),o?(t&&r.log(r.asProcess("updating an interest group")),s=await v.store.put(I,o,{_expired:Date.now()+n,...e})):(t&&r.log(r.asProcess("creating a new interest group")),s=await v.store.add(I,{_key:D(e.owner,e.name),_expired:Date.now()+n,...e})),t&&r.log(r.asSuccess("interest group id:"),s),t&&r.groupEnd(),!0}(t,o,s),!0}case"leaveAdInterestGroup":{const[,n]=e,[t,o]=n;return await async function(e,n){return n&&r.groupCollapsed("Fledge API: leaveAdInterest"),n&&r.log(r.asProcess("deleting an existing interest group")),await v.store.delete(I,D(e.owner,e.name)),n&&r.log(r.asSuccess("interest group deleted")),n&&r.groupEnd(),!0}(t,o),!0}case"runAdAuction":{const[,t]=e,[o,r]=t;if(1!==n.length)throw new Error(`Port transfer mismatch during request: expected 1 port, but received ${n.length}`);const[s]=n,a=[!0,await x(o,r)];return s.postMessage(a),s.close(),!0}default:return!1}}catch(e){const t=[!1];for(const e of n)e.postMessage(t);throw e}}return async function(){const{searchParams:e}=new URL(window.location),n=e.get("debug")||!1;if(n&&r.group("Fledge: Storage Frame"),!(e.get("admin")||!1)){const[e]=window.location.ancestorOrigins;if(void 0===e)throw n&&r.log(r.asWarning("It appears your attempting to access this from the top-level document")),n&&r.log({origin:e,location:window.location}),new Error("Can't call 'postMessage' on the Frame window when run as a top-level document");const{port1:t,port2:o}=new MessageChannel;n&&r.log("message channel receiver:",t),n&&r.log("message channel sender:",o),t.onmessage=k,window.parent.postMessage({"fledge.polyfill":1},e,[o])}n&&r.groupEnd()}}(); +var fledgeframe=function(){"use strict";let e=[];const o={},n="%c ";function t(t){return function(...r){const s=[],a=[];r.forEach((t=>{if(t===o){const o=e.shift();s.push(`%c${o.value}`,n),a.push(o.css,"")}else"object"==typeof t||"function"==typeof t?(s.push("%o",n),a.push(t,"")):(s.push(`%c${t}`,n),a.push("",""))})),t(s.join(""),...a),e=[]}}const r={assert:t(console.assert),clear:t(console.clear),count:t(console.count),countReset:t(console.countReset),debug:t(console.debug),dir:t(console.dir),error:t(console.error),group:t(console.group),groupCollapsed:t(console.groupCollapsed),groupEnd:t(console.groupEnd),info:t(console.info),log:t(console.log),table:t(console.table),time:t(console.time),timeEnd:t(console.timeEnd),timeLog:t(console.timeLog),trace:t(console.trace),warn:t(console.warn),asAlert:function(n){return e.push({value:n,css:"display: inline-block; background-color: #dc3545; color: #ffffff; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),o},asInfo:function(n){return e.push({value:n,css:"color: #0366d6; font-weight: bold;"}),o},asProcess:function(n){return e.push({value:`${n}…`,css:"color: #8c8c8c; font-style: italic;"}),o},asSuccess:function(n){return e.push({value:n,css:"color: #289d45; font-weight: bold;"}),o},asWarning:function(n){return e.push({value:n,css:"display: inline-block; background-color: #ffc107; color: black; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),o}};function s(e){return new Promise(((o,n)=>{e.oncomplete=e.onsuccess=()=>o(e.result),e.onabort=e.onerror=()=>n(e.error)}))}let a;function i(){return a||(a=function(e,o){const n=indexedDB.open(e);n.onupgradeneeded=()=>n.result.createObjectStore(o);const t=s(n);return(e,n)=>t.then((t=>n(t.transaction(o,e).objectStore(o))))}("keyval-store","keyval")),a}function c(e=i()){const o=[];return function(e,o){return e("readonly",(e=>(e.openCursor().onsuccess=function(){this.result&&(o(this.result),this.result.continue())},s(e.transaction))))}(e,(e=>o.push([e.key,e.value]))).then((()=>o))}const l=async(e,o,n)=>{n&&r.groupCollapsed("auction utils: getTrustedSignals");const t=`hostname=${window.top.location.hostname}`;if(!e||!o)return n&&r.log(r.asWarning("No 'url' or 'keys' found!")),void(n&&r.groupEnd());const s=await fetch(`${e}?${t}&keys=${o.join(",")}`).then((e=>{if(!e.ok)throw new Error("Something went wrong! The response returned was not ok.");if(!(e=>/\bapplication\/json\b/.test(e?.headers?.get("content-type")))(e))throw new Error("Response was not in the format of JSON.");return e.json()})).catch((e=>(n&&r.log(r.asAlert("There was a problem with your fetch operation:")),n&&r.log(e),null))),a={};for(const[e,n]of s)o.includes(e)&&(a[e]=n);return a&&0===Object.keys(a).length&&a.constructor===Object?(n&&r.groupEnd(),a):(n&&r.log(r.asWarning("No signals found!")),n&&r.groupEnd(),null)};async function u(e,o){o&&r.groupCollapsed("Fledge API: runAdAuction");const n=await c();o&&r.log(r.asInfo("all interest groups:"),n);const t=((e,o,n)=>{if(n&&r.groupCollapsed("auction utils: getEligible"),"*"===o)return n&&r.info("using the wildcard yields all groups"),n&&r.groupEnd(),e;const t=e.filter((([e,n])=>o.includes(n.owner)));return t.length?(n&&r.info("found some eligible buyers"),n&&r.groupEnd(),t):(n&&r.log(r.asWarning("No groups were eligible!")),n&&r.groupEnd(),null)})(n,e.interest_group_buyers,o);if(o&&r.log(r.asInfo('eligible buyers based on "interest_group_buyers":'),t),!t)return o&&r.log(r.asAlert("No eligible interest group buyers found!")),null;const s=await(async(e,o,n)=>Promise.all(e.map((async([e,t])=>{n&&r.groupCollapsed(`auction utils: getBids => ${e}`);const s=performance.now(),{generateBid:a,generate_bid:i}=await import(t.bidding_logic_url);let c=a;if(i&&!a&&(c=i),!c&&"function"!=typeof c)return n&&r.log(r.asWarning("No 'generateBid' function found!")),n&&r.groupEnd(),null;const u=await l(t?.trusted_bidding_signals_url,t?.trusted_bidding_signals_keys,n);let g;try{g=c(t,o?.auction_signals,o?.per_buyer_signals?.[t.owner],u,{top_window_hostname:window.top.location.hostname,seller:o.seller}),n&&r.log(r.asInfo("bid:"),g)}catch(e){return n&&r.log(r.asAlert("There was an error in the 'generateBid' function:")),n&&r.log(e),null}if(!(g.ad&&"object"==typeof g.ad&&g.bid&&"number"==typeof g.bid&&g.render)||"string"!=typeof g.render&&!Array.isArray(g.render))return n&&r.log(r.asWarning("No bid found!")),n&&r.groupEnd(),null;const d=performance.now();return n&&r.groupEnd(),{...t,...g,duration:d-s}}))))(t,e,o);o&&r.log(r.asInfo("all bids from each buyer:"),s);const a=s.filter((e=>e));if(o&&r.log(r.asInfo("filtered bids:"),a),!a.length)return o&&r.log(r.asAlert("No bids found!")),o&&r.groupEnd(),null;o&&r.log(r.asProcess("getting all scores, filtering and sorting"));const[i]=await(async(e,o,n)=>{n&&r.groupCollapsed("auction utils: getScores");const{scoreAd:t,score_ad:s}=await import(o.decision_logic_url);let a=t;return s&&!t&&(a=s),a||"function"==typeof a?e.map((e=>{let t;try{t=a(e?.ad,e?.bid,o,o?.trusted_scoring_signals,{top_window_hostname:window.top.location.hostname,interest_group_owner:e.owner,interest_group_name:e.name,bidding_duration_msec:e.duration}),n&&r.log(r.asInfo("score:"),t)}catch(e){n&&r.log(r.asAlert("There was an error in the 'scoreAd' function:")),n&&r.log(e),t=-1}return n&&r.groupEnd(),{bid:e,score:t}})).filter((({score:e})=>e>0)).sort(((e,o)=>e.score>o.score?1:-1)):(n&&r.log(r.asWarning("No 'scoreAd' function was found!")),null)})(a,e,o);if(o&&r.log(r.asInfo("winner:"),i),!i)return o&&r.log(r.asAlert("No winner found!")),o&&r.groupEnd(),null;o&&r.log(r.asProcess("creating an entry in the auction store"));const u=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(e=>(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)));return sessionStorage.setItem(u,JSON.stringify({origin:`${window.top.location.origin}${window.top.location.pathname}`,timestamp:Date.now(),conf:e,...i})),o&&r.log(r.asSuccess("auction token:"),u),o&&r.groupEnd(),u}const g=(e,o)=>`${e}-${o}`;async function d(e,o,n){n&&r.groupCollapsed("Fledge API: joinAdInterest");const t=g(e.owner,e.name),a=await function(e,o=i()){return o("readonly",(o=>s(o.get(e))))}(t);return n&&r.log(r.asInfo("checking for an existing interest group:"),a),a?(n&&r.log(r.asProcess("updating an interest group")),await function(e,o,n=i()){return n("readwrite",(n=>new Promise(((t,r)=>{n.get(e).onsuccess=function(){try{n.put(o(this.result),e),t(s(n.transaction))}catch(e){r(e)}}}))))}(t,{_expired:Date.now()+o,...e})):(n&&r.log(r.asProcess("creating a new interest group")),await function(e,o,n=i()){return n("readwrite",(n=>(n.put(o,e),s(n.transaction))))}(t,{_created:Date.now(),_expired:Date.now()+o,_updated:Date.now(),...e})),n&&r.log(r.asSuccess("interest group id:"),t),n&&r.groupEnd(),!0}async function p(e,o){return o&&r.groupCollapsed("Fledge API: leaveAdInterest"),o&&r.log(r.asProcess("deleting an existing interest group")),await function(e,o=i()){return o("readwrite",(o=>(o.delete(e),s(o.transaction))))}(g(e.owner,e.name)),o&&r.log(r.asSuccess("interest group deleted")),o&&r.groupEnd(),!0}async function f({data:e,ports:o}){try{if(!Array.isArray(e))throw new Error(`The API expects the data to be in the form of an array, with index 0 set to the action, and index 1 set to the data. A ${typeof e} was passed instead.`);switch(e[0]){case"joinAdInterestGroup":{const[,o]=e,[n,t,r]=o;return await d(n,t,r),!0}case"leaveAdInterestGroup":{const[,o]=e,[n,t]=o;return await p(n,t),!0}case"runAdAuction":{const[,n]=e,[t,r]=n;if(1!==o.length)throw new Error(`Port transfer mismatch during request: expected 1 port, but received ${o.length}`);const[s]=o,a=[!0,await u(t,r)];return s.postMessage(a),s.close(),!0}default:return!1}}catch(e){const n=[!1];for(const e of o)e.postMessage(n);throw e}}return async function(){const{searchParams:e}=new URL(window.location),o=e.get("debug")||!1;if(o&&r.group("Fledge: Storage Frame"),!(e.get("admin")||!1)){const[e]=window.location.ancestorOrigins;if(void 0===e)throw o&&r.log(r.asWarning("It appears your attempting to access this from the top-level document")),o&&r.log({origin:e,location:window.location}),new Error("Can't call 'postMessage' on the Frame window when run as a top-level document");const{port1:n,port2:t}=new MessageChannel;o&&r.log("message channel receiver:",n),o&&r.log("message channel sender:",t),n.onmessage=f,window.parent.postMessage({"fledge.polyfill":1},e,[t])}o&&r.groupEnd()}}(); diff --git a/package-lock.json b/package-lock.json index d7064ac..3d26fe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10639,10 +10639,10 @@ "safer-buffer": ">= 2.1.2 < 3" } }, - "idb": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/idb/-/idb-6.0.0.tgz", - "integrity": "sha512-+M367poGtpzAylX4pwcrZIa7cFQLfNkAOlMMLN2kw/2jGfJP6h+TB/unQNSVYwNtP8XqkLYrfuiVnxLQNP1tjA==" + "idb-keyval": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-5.0.5.tgz", + "integrity": "sha512-cqi65rrjhgPExI9vmSU7VcYEbHCUfIBY+9YUWxyr0PyGizptFgGFnvZQ0w+tqOXk1lUcGCZGVLfabf7QnR2S0g==" }, "ieee754": { "version": "1.2.1", diff --git a/package.json b/package.json index afa95f9..3ac9e88 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,6 @@ }, "dependencies": { "@theholocron/klaxon": "^1.1.0", - "idb": "^6.0.0" + "idb-keyval": "^5.0.5" } } diff --git a/src/frame/api.js b/src/frame/api.js index 01ffc60..94c5f18 100644 --- a/src/frame/api.js +++ b/src/frame/api.js @@ -1,8 +1,8 @@ -import runAdAuction from './auction/index.js'; +import runAdAuction from './auction/'; import { joinAdInterestGroup, leaveAdInterestGroup, -} from './interest-groups/index.js'; +} from './interest-groups/'; export default async function fledgeAPI ({ data, ports }) { try { diff --git a/src/frame/auction/index.js b/src/frame/auction/index.js index 4e8359f..a94cfc7 100644 --- a/src/frame/auction/index.js +++ b/src/frame/auction/index.js @@ -1,11 +1,11 @@ import { echo } from '@theholocron/klaxon'; -import db, { IG_STORE } from '../utils.js'; +import * as idb from 'idb-keyval'; import { getBids, getEligible, getScores, uuid, -} from './utils.js'; +} from './utils'; /* * @function @@ -21,7 +21,7 @@ import { */ export default async function runAdAuction (conf, debug) { debug && echo.groupCollapsed('Fledge API: runAdAuction'); - const interestGroups = await db.store.getAll(IG_STORE); + const interestGroups = await idb.entries(); debug && echo.log(echo.asInfo('all interest groups:'), interestGroups); const eligible = getEligible(interestGroups, conf.interest_group_buyers, debug); diff --git a/src/frame/auction/utils.js b/src/frame/auction/utils.js index 9446c30..abc063f 100644 --- a/src/frame/auction/utils.js +++ b/src/frame/auction/utils.js @@ -18,7 +18,7 @@ export const getEligible = (groups, eligibility, debug) => { return groups; } - const eligible = groups.filter(({ owner }) => eligibility.includes(owner)); + const eligible = groups.filter(([ key, value ]) => eligibility.includes(value.owner)); if (eligible.length) { debug && echo.info(`found some eligible buyers`); debug && echo.groupEnd(); @@ -40,8 +40,8 @@ export const getEligible = (groups, eligibility, debug) => { * @return {object | null} an array of objects containing bids; null if none found */ export const getBids = async (bidders, conf, debug) => Promise.all( - bidders.map(async bidder => { - debug && echo.groupCollapsed(`auction utils: getBids => ${bidder._key}`); + bidders.map(async ([ key, bidder ]) => { + debug && echo.groupCollapsed(`auction utils: getBids => ${key}`); const time0 = performance.now(); const { generateBid, generate_bid } = await import(bidder.bidding_logic_url); let callBid = generateBid; diff --git a/src/frame/interest-groups/index.js b/src/frame/interest-groups/index.js index dc85f50..4d2957e 100644 --- a/src/frame/interest-groups/index.js +++ b/src/frame/interest-groups/index.js @@ -1,5 +1,5 @@ import { echo } from '@theholocron/klaxon'; -import db, { IG_STORE } from '../utils.js'; +import * as idb from 'idb-keyval'; /* * @function @@ -31,20 +31,21 @@ export const getIGKey = (owner, name) => `${owner}-${name}`; */ export async function joinAdInterestGroup (options, expiry, debug) { debug && echo.groupCollapsed('Fledge API: joinAdInterest'); - const group = await db.store.get(IG_STORE, getIGKey(options.owner, options.name)); + const id = getIGKey(options.owner, options.name); + const group = await idb.get(id); debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); - let id; if (group) { debug && echo.log(echo.asProcess('updating an interest group')); - id = await db.store.put(IG_STORE, group, { + await idb.update(id, { _expired: Date.now() + expiry, ...options, }); } else { debug && echo.log(echo.asProcess('creating a new interest group')); - id = await db.store.add(IG_STORE, { - _key: getIGKey(options.owner, options.name), + await idb.set(id, { + _created: Date.now(), _expired: Date.now() + expiry, + _updated: Date.now(), ...options, }); } @@ -69,7 +70,7 @@ export async function joinAdInterestGroup (options, expiry, debug) { export async function leaveAdInterestGroup (group, debug) { debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); debug && echo.log(echo.asProcess('deleting an existing interest group')); - await db.store.delete(IG_STORE, getIGKey(group.owner, group.name)); + await idb.del(getIGKey(group.owner, group.name)); debug && echo.log(echo.asSuccess('interest group deleted')); debug && echo.groupEnd(); diff --git a/src/frame/utils.js b/src/frame/utils.js deleted file mode 100644 index db48b32..0000000 --- a/src/frame/utils.js +++ /dev/null @@ -1,142 +0,0 @@ -import { openDB } from 'idb'; - -/* - * @const {string} - * @summary the name of the Interest Group store within IndexedDB - */ -export const IG_STORE = 'interest-groups'; - -/* - * @function - * @name db - * @description create an Indexed dB - * @author Newton - * @return {promise} a promise - */ -const db = openDB('Fledge', 1, { - upgrade (db) { - // Create a store of objects - const igStore = db.createObjectStore(IG_STORE, { - // The '_key' property of the object will be the key. - keyPath: '_key', - }); - - // Create an index on the a few properties of the objects. - [ 'owner', 'name', '_expired' ].forEach(index => { - igStore.createIndex(index, index, { unique: false }); - }); - }, -}); - -/* - * @function - * @name getItemFromStore - * @description retrieve an item from an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {string} id - the id; typically matches the keyPath of a store - * @return {object} an object representing an interest group - * - * @example - * store.get('someStore', 'foo'); - */ -async function getItemFromStore (store, id) { - const item = (await db).get(store, id); - - if (item) { - return item; - } - - return null; -} - -/* - * @function - * @name getAllFromStore - * @description retrieve all items from an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @return {array} an array of objects representing all items from a store - * - * @example - * store.getAll('someStore'); - */ -async function getAllFromStore (store) { - const items = (await db).getAll(store); - - if (items) { - return items; - } - - return null; -} - -/* - * @function - * @name updateItemInStore - * @description update an item in an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {object} item - An existing item - * @param {object} newOptions - a new set of options to merge with the item - * @return {string} the key of the item updated - * - * @example - * store.put('someStore', { bidding_logic_url: '://v2/bid' }, { owner: 'foo', name: 'bar' }, 1234); - */ -async function updateItemInStore (store, item, newOptions) { - const updated = { - ...item, - ...newOptions, - _updated: Date.now(), - }; - - return (await db).put(store, updated); -} - -/* - * @function - * @name createItemInStore - * @description create an item in an IDB store - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {object} options - An object of options to make up item - * @return {string} the key of the item created - * - * @example - * store.add('someStore', { owner: 'foo', name: 'bar' }); - */ -async function createItemInStore (store, options) { - return (await db).add(store, { - ...options, - _created: Date.now(), - _updated: Date.now(), - }); -} - -/* - * @function - * @name deleteItemFromStore - * @description delete a record from Indexed dB - * @author Newton - * @param {string} store - the name of the store from which to retreive - * @param {string} id - the id; typically matches the keyPath of a store - * @return {undefined} - * - * @example - * store.delete('owner-name'); - */ -async function deleteItemFromStore (store, id) { - return (await db).delete(store, id); -} - -export default { - db, - store: { - add: createItemInStore, - get: getItemFromStore, - getAll: getAllFromStore, - put: updateItemInStore, - delete: deleteItemFromStore, - }, -}; diff --git a/test/e2e/index.html b/test/e2e/index.html index 0aa72fa..0ec9921 100644 --- a/test/e2e/index.html +++ b/test/e2e/index.html @@ -4,6 +4,6 @@ Fledge - + diff --git a/test/e2e/interest-groups.test.js b/test/e2e/interest-groups.test.js index 55408f0..dbe00d2 100644 --- a/test/e2e/interest-groups.test.js +++ b/test/e2e/interest-groups.test.js @@ -48,10 +48,10 @@ describe('Fledge', () => { await page.goto('http://localhost:3000/test/e2e/'); const result = await page.evaluate(() => new Promise(resolve => { - const request = window.indexedDB.open('Fledge'); + const request = window.indexedDB.open('keyval-store'); request.onsuccess = () => { const db = request.result; - db.transaction('interest-groups', 'readonly').objectStore('interest-groups').get('magnite.com-test-interest').onsuccess = function (event) { + db.transaction('keyval', 'readonly').objectStore('keyval').get('magnite.com-test-interest').onsuccess = function (event) { resolve(event.target.result); }; db.close(); diff --git a/test/unit/api/render.error.test.js b/test/unit/api/render.error.test.js index 491cb6f..2a26e09 100644 --- a/test/unit/api/render.error.test.js +++ b/test/unit/api/render.error.test.js @@ -1,54 +1,6 @@ import renderAd from '../../../src/api/render'; import { mockAuctionResults } from '../../mocks/auction.mock'; -jest.mock('../../../src/frame/utils', () => ({ - store: { - get: () => new Promise(resolve => { - resolve({ - id: 'c6b3fd61-4d16-44d1-9364-acc9ceb286f3', - origin: 'http://localhost/', - timestamp: 1619635510421, - conf: { - seller: 'www.ssp.com', - interest_group_buyers: '*', - decision_logic_url: 'https://entertaining-glitter-bowler.glitch.me/score.js', - }, - bid: { - _key: 'www.rp.com-womens-running-shoes', - _expired: 1621792745000, - owner: 'www.rp.com', - name: 'womens-running-shoes', - bidding_logic_url: 'https://dark-organic-appeal.glitch.me/bid.js', - _created: 1619200745000, - _updated: 1619200745000, - ad: { - auction: {}, - browser: { - top_window_hostname: 'localhost', - seller: 'www.ssp.com', - }, - buyer: {}, - interest: { - _key: 'www.rp.com-womens-running-shoes', - _expired: 1621792745000, - owner: 'www.rp.com', - name: 'womens-running-shoes', - bidding_logic_url: 'https://dark-organic-appeal.glitch.me/bid.js', - _created: 1619200745000, - _updated: 1619200745000, - }, - }, - bid: 1, - render: 'https://example.com', - }, - score: 10, - _created: 1619635510421, - _updated: 1619635510421, - }); - }), - }, -})); - jest.mock('../../../src/api/utils', () => ({ frame: { create: () => '
', From dde3caae65f76b242b5de371395cb763160d5da3 Mon Sep 17 00:00:00 2001 From: Newton Koumantzelis Date: Thu, 13 May 2021 11:23:02 -0700 Subject: [PATCH 2/5] chore: fix tests --- test/mocks/interest-groups.mock.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/mocks/interest-groups.mock.js b/test/mocks/interest-groups.mock.js index 512bfce..81064e1 100644 --- a/test/mocks/interest-groups.mock.js +++ b/test/mocks/interest-groups.mock.js @@ -39,9 +39,15 @@ export const mockAllOptions = { }; export const mockIGDb = [ - mockAllOptions, - { - ...mockAllOptions, - owner: 'new-mock-owner.com', - }, + [ + `${mockOwner}-${mockName}`, + mockAllOptions, + ], + [ + `new-mock-owner.com-${mockName}`, + { + ...mockAllOptions, + owner: 'new-mock-owner.com', + }, + ], ]; From 4c7f4d49e0c5027fc8030570199f434c1f026466 Mon Sep 17 00:00:00 2001 From: Newton Koumantzelis Date: Thu, 13 May 2021 11:39:02 -0700 Subject: [PATCH 3/5] feat: create custom store --- src/frame/auction/index.js | 3 ++- src/frame/interest-groups/index.js | 10 ++++++---- test/e2e/interest-groups.test.js | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/frame/auction/index.js b/src/frame/auction/index.js index a94cfc7..61213a0 100644 --- a/src/frame/auction/index.js +++ b/src/frame/auction/index.js @@ -1,5 +1,6 @@ import { echo } from '@theholocron/klaxon'; import * as idb from 'idb-keyval'; +import { customStore } from '../interest-groups/'; import { getBids, getEligible, @@ -21,7 +22,7 @@ import { */ export default async function runAdAuction (conf, debug) { debug && echo.groupCollapsed('Fledge API: runAdAuction'); - const interestGroups = await idb.entries(); + const interestGroups = await idb.entries(customStore); debug && echo.log(echo.asInfo('all interest groups:'), interestGroups); const eligible = getEligible(interestGroups, conf.interest_group_buyers, debug); diff --git a/src/frame/interest-groups/index.js b/src/frame/interest-groups/index.js index 4d2957e..b60f4af 100644 --- a/src/frame/interest-groups/index.js +++ b/src/frame/interest-groups/index.js @@ -1,6 +1,8 @@ import { echo } from '@theholocron/klaxon'; import * as idb from 'idb-keyval'; +export const customStore = idb.createStore('fledge.v1', 'interest-groups'); + /* * @function * @name getIGKey @@ -32,14 +34,14 @@ export const getIGKey = (owner, name) => `${owner}-${name}`; export async function joinAdInterestGroup (options, expiry, debug) { debug && echo.groupCollapsed('Fledge API: joinAdInterest'); const id = getIGKey(options.owner, options.name); - const group = await idb.get(id); + const group = await idb.get(id, customStore); debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); if (group) { debug && echo.log(echo.asProcess('updating an interest group')); await idb.update(id, { _expired: Date.now() + expiry, ...options, - }); + }, customStore); } else { debug && echo.log(echo.asProcess('creating a new interest group')); await idb.set(id, { @@ -47,7 +49,7 @@ export async function joinAdInterestGroup (options, expiry, debug) { _expired: Date.now() + expiry, _updated: Date.now(), ...options, - }); + }, customStore); } debug && echo.log(echo.asSuccess('interest group id:'), id); debug && echo.groupEnd(); @@ -70,7 +72,7 @@ export async function joinAdInterestGroup (options, expiry, debug) { export async function leaveAdInterestGroup (group, debug) { debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); debug && echo.log(echo.asProcess('deleting an existing interest group')); - await idb.del(getIGKey(group.owner, group.name)); + await idb.del(getIGKey(group.owner, group.name), customStore); debug && echo.log(echo.asSuccess('interest group deleted')); debug && echo.groupEnd(); diff --git a/test/e2e/interest-groups.test.js b/test/e2e/interest-groups.test.js index dbe00d2..fd705ff 100644 --- a/test/e2e/interest-groups.test.js +++ b/test/e2e/interest-groups.test.js @@ -48,10 +48,10 @@ describe('Fledge', () => { await page.goto('http://localhost:3000/test/e2e/'); const result = await page.evaluate(() => new Promise(resolve => { - const request = window.indexedDB.open('keyval-store'); + const request = window.indexedDB.open('fledge.v1'); request.onsuccess = () => { const db = request.result; - db.transaction('keyval', 'readonly').objectStore('keyval').get('magnite.com-test-interest').onsuccess = function (event) { + db.transaction('interest-groups', 'readonly').objectStore('interest-groups').get('magnite.com-test-interest').onsuccess = function (event) { resolve(event.target.result); }; db.close(); From b2bd8e06ca5175336d8d948d1fcd7f103af363b1 Mon Sep 17 00:00:00 2001 From: Newton Koumantzelis Date: Thu, 13 May 2021 11:39:22 -0700 Subject: [PATCH 4/5] chore: update docs --- docs/interest-groups.html | 3 +- docs/scripts/frame/cjs/index.js | 156 ++++++++++++++------------- docs/scripts/frame/esm/index.js | 156 ++++++++++++++------------- docs/scripts/frame/iife/index.min.js | 2 +- 4 files changed, 161 insertions(+), 156 deletions(-) diff --git a/docs/interest-groups.html b/docs/interest-groups.html index 035e365..b7390d6 100644 --- a/docs/interest-groups.html +++ b/docs/interest-groups.html @@ -44,6 +44,7 @@

Currently Joined

import * as idb from 'https://cdn.jsdelivr.net/npm/idb-keyval@5/dist/esm/index.js'; import Fledge from './scripts/api/esm/index.js'; const fledge = new Fledge(true); + const customStore = idb.createStore('fledge.v1', 'interest-groups'); document.getElementById("join-ig").onclick = async function joinGroup() { await fledge.joinAdInterestGroup({ @@ -61,7 +62,7 @@

Currently Joined

}, parseInt(document.getElementById('expiration').value), true); }; - const interestGroups = await idb.entries(); + const interestGroups = await idb.entries(customStore); if (interestGroups.length > 0) { const list = document.createElement("ul"); interestGroups.map(([ key, value ]) => { diff --git a/docs/scripts/frame/cjs/index.js b/docs/scripts/frame/cjs/index.js index 06e2320..7f25c74 100644 --- a/docs/scripts/frame/cjs/index.js +++ b/docs/scripts/frame/cjs/index.js @@ -255,6 +255,84 @@ function entries(customStore = defaultGetStore()) { return eachCursor(customStore, (cursor) => items.push([cursor.key, cursor.value])).then(() => items); } +const customStore = createStore('fledge.v1', 'interest-groups'); + +/* + * @function + * @name getIGKey + * @description retrieve the key for an interest group form the store + * @author Newton + * @param {string} owner - owner of the interest group + * @param {string} name - name of the interest group + * @return {object} an object representing an interest group + * + * @example + * getKey('foo', 'bar'); + * // 'foo-bar' + */ +const getIGKey = (owner, name) => `${owner}-${name}`; + +/* + * @function + * @name joinAdInterestGroup + * @description join an interest group inserting into IndexedDB + * @author Newton + * @param {object} options - An object of options to create an interest group {@link types} + * @param {number} expiry - A number of the days (in milliseconds) an interest group should exist, not to exceed 30 days + * @throws {Error} Any parameters passed are incorrect or an incorrect type + * @return {true} + * + * @example + * joinAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }, 2592000000); + */ +async function joinAdInterestGroup (options, expiry, debug) { + debug && echo.groupCollapsed('Fledge API: joinAdInterest'); + const id = getIGKey(options.owner, options.name); + const group = await get(id, customStore); + debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); + if (group) { + debug && echo.log(echo.asProcess('updating an interest group')); + await update(id, { + _expired: Date.now() + expiry, + ...options, + }, customStore); + } else { + debug && echo.log(echo.asProcess('creating a new interest group')); + await set(id, { + _created: Date.now(), + _expired: Date.now() + expiry, + _updated: Date.now(), + ...options, + }, customStore); + } + debug && echo.log(echo.asSuccess('interest group id:'), id); + debug && echo.groupEnd(); + + return true; +} + +/* + * @function + * @name leaveAdInterestGroup + * @description leave an interest group removing from IndexedDB + * @author Newton + * @param {object} options - An object of options to create an interest group {@link types} + * @throws {Error} Any parameters passed are incorrect or an incorrect type + * @return {true} + * + * @example + * leaveAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }); + */ +async function leaveAdInterestGroup (group, debug) { + debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); + debug && echo.log(echo.asProcess('deleting an existing interest group')); + await del(getIGKey(group.owner, group.name), customStore); + debug && echo.log(echo.asSuccess('interest group deleted')); + debug && echo.groupEnd(); + + return true; +} + /* eslint-disable camelcase */ /* @@ -485,7 +563,7 @@ const getTrustedSignals = async (url, keys, debug) => { */ async function runAdAuction (conf, debug) { debug && echo.groupCollapsed('Fledge API: runAdAuction'); - const interestGroups = await entries(); + const interestGroups = await entries(customStore); debug && echo.log(echo.asInfo('all interest groups:'), interestGroups); const eligible = getEligible(interestGroups, conf.interest_group_buyers, debug); @@ -529,82 +607,6 @@ async function runAdAuction (conf, debug) { return token; } -/* - * @function - * @name getIGKey - * @description retrieve the key for an interest group form the store - * @author Newton - * @param {string} owner - owner of the interest group - * @param {string} name - name of the interest group - * @return {object} an object representing an interest group - * - * @example - * getKey('foo', 'bar'); - * // 'foo-bar' - */ -const getIGKey = (owner, name) => `${owner}-${name}`; - -/* - * @function - * @name joinAdInterestGroup - * @description join an interest group inserting into IndexedDB - * @author Newton - * @param {object} options - An object of options to create an interest group {@link types} - * @param {number} expiry - A number of the days (in milliseconds) an interest group should exist, not to exceed 30 days - * @throws {Error} Any parameters passed are incorrect or an incorrect type - * @return {true} - * - * @example - * joinAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }, 2592000000); - */ -async function joinAdInterestGroup (options, expiry, debug) { - debug && echo.groupCollapsed('Fledge API: joinAdInterest'); - const id = getIGKey(options.owner, options.name); - const group = await get(id); - debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); - if (group) { - debug && echo.log(echo.asProcess('updating an interest group')); - await update(id, { - _expired: Date.now() + expiry, - ...options, - }); - } else { - debug && echo.log(echo.asProcess('creating a new interest group')); - await set(id, { - _created: Date.now(), - _expired: Date.now() + expiry, - _updated: Date.now(), - ...options, - }); - } - debug && echo.log(echo.asSuccess('interest group id:'), id); - debug && echo.groupEnd(); - - return true; -} - -/* - * @function - * @name leaveAdInterestGroup - * @description leave an interest group removing from IndexedDB - * @author Newton - * @param {object} options - An object of options to create an interest group {@link types} - * @throws {Error} Any parameters passed are incorrect or an incorrect type - * @return {true} - * - * @example - * leaveAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }); - */ -async function leaveAdInterestGroup (group, debug) { - debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); - debug && echo.log(echo.asProcess('deleting an existing interest group')); - await del(getIGKey(group.owner, group.name)); - debug && echo.log(echo.asSuccess('interest group deleted')); - debug && echo.groupEnd(); - - return true; -} - async function fledgeAPI ({ data, ports }) { try { if (!Array.isArray(data)) { diff --git a/docs/scripts/frame/esm/index.js b/docs/scripts/frame/esm/index.js index a005086..71e6c09 100644 --- a/docs/scripts/frame/esm/index.js +++ b/docs/scripts/frame/esm/index.js @@ -233,6 +233,84 @@ function entries(customStore = defaultGetStore()) { return eachCursor(customStore, (cursor) => items.push([cursor.key, cursor.value])).then(() => items); } +const customStore = createStore('fledge.v1', 'interest-groups'); + +/* + * @function + * @name getIGKey + * @description retrieve the key for an interest group form the store + * @author Newton + * @param {string} owner - owner of the interest group + * @param {string} name - name of the interest group + * @return {object} an object representing an interest group + * + * @example + * getKey('foo', 'bar'); + * // 'foo-bar' + */ +const getIGKey = (owner, name) => `${owner}-${name}`; + +/* + * @function + * @name joinAdInterestGroup + * @description join an interest group inserting into IndexedDB + * @author Newton + * @param {object} options - An object of options to create an interest group {@link types} + * @param {number} expiry - A number of the days (in milliseconds) an interest group should exist, not to exceed 30 days + * @throws {Error} Any parameters passed are incorrect or an incorrect type + * @return {true} + * + * @example + * joinAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }, 2592000000); + */ +async function joinAdInterestGroup (options, expiry, debug) { + debug && echo.groupCollapsed('Fledge API: joinAdInterest'); + const id = getIGKey(options.owner, options.name); + const group = await get(id, customStore); + debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); + if (group) { + debug && echo.log(echo.asProcess('updating an interest group')); + await update(id, { + _expired: Date.now() + expiry, + ...options, + }, customStore); + } else { + debug && echo.log(echo.asProcess('creating a new interest group')); + await set(id, { + _created: Date.now(), + _expired: Date.now() + expiry, + _updated: Date.now(), + ...options, + }, customStore); + } + debug && echo.log(echo.asSuccess('interest group id:'), id); + debug && echo.groupEnd(); + + return true; +} + +/* + * @function + * @name leaveAdInterestGroup + * @description leave an interest group removing from IndexedDB + * @author Newton + * @param {object} options - An object of options to create an interest group {@link types} + * @throws {Error} Any parameters passed are incorrect or an incorrect type + * @return {true} + * + * @example + * leaveAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }); + */ +async function leaveAdInterestGroup (group, debug) { + debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); + debug && echo.log(echo.asProcess('deleting an existing interest group')); + await del(getIGKey(group.owner, group.name), customStore); + debug && echo.log(echo.asSuccess('interest group deleted')); + debug && echo.groupEnd(); + + return true; +} + /* eslint-disable camelcase */ /* @@ -463,7 +541,7 @@ const getTrustedSignals = async (url, keys, debug) => { */ async function runAdAuction (conf, debug) { debug && echo.groupCollapsed('Fledge API: runAdAuction'); - const interestGroups = await entries(); + const interestGroups = await entries(customStore); debug && echo.log(echo.asInfo('all interest groups:'), interestGroups); const eligible = getEligible(interestGroups, conf.interest_group_buyers, debug); @@ -507,82 +585,6 @@ async function runAdAuction (conf, debug) { return token; } -/* - * @function - * @name getIGKey - * @description retrieve the key for an interest group form the store - * @author Newton - * @param {string} owner - owner of the interest group - * @param {string} name - name of the interest group - * @return {object} an object representing an interest group - * - * @example - * getKey('foo', 'bar'); - * // 'foo-bar' - */ -const getIGKey = (owner, name) => `${owner}-${name}`; - -/* - * @function - * @name joinAdInterestGroup - * @description join an interest group inserting into IndexedDB - * @author Newton - * @param {object} options - An object of options to create an interest group {@link types} - * @param {number} expiry - A number of the days (in milliseconds) an interest group should exist, not to exceed 30 days - * @throws {Error} Any parameters passed are incorrect or an incorrect type - * @return {true} - * - * @example - * joinAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }, 2592000000); - */ -async function joinAdInterestGroup (options, expiry, debug) { - debug && echo.groupCollapsed('Fledge API: joinAdInterest'); - const id = getIGKey(options.owner, options.name); - const group = await get(id); - debug && echo.log(echo.asInfo('checking for an existing interest group:'), group); - if (group) { - debug && echo.log(echo.asProcess('updating an interest group')); - await update(id, { - _expired: Date.now() + expiry, - ...options, - }); - } else { - debug && echo.log(echo.asProcess('creating a new interest group')); - await set(id, { - _created: Date.now(), - _expired: Date.now() + expiry, - _updated: Date.now(), - ...options, - }); - } - debug && echo.log(echo.asSuccess('interest group id:'), id); - debug && echo.groupEnd(); - - return true; -} - -/* - * @function - * @name leaveAdInterestGroup - * @description leave an interest group removing from IndexedDB - * @author Newton - * @param {object} options - An object of options to create an interest group {@link types} - * @throws {Error} Any parameters passed are incorrect or an incorrect type - * @return {true} - * - * @example - * leaveAdInterestGroup({ owner: 'foo', name: 'bar', bidding_logic_url: 'http://example.com/bid' }); - */ -async function leaveAdInterestGroup (group, debug) { - debug && echo.groupCollapsed('Fledge API: leaveAdInterest'); - debug && echo.log(echo.asProcess('deleting an existing interest group')); - await del(getIGKey(group.owner, group.name)); - debug && echo.log(echo.asSuccess('interest group deleted')); - debug && echo.groupEnd(); - - return true; -} - async function fledgeAPI ({ data, ports }) { try { if (!Array.isArray(data)) { diff --git a/docs/scripts/frame/iife/index.min.js b/docs/scripts/frame/iife/index.min.js index f428d5c..c408250 100644 --- a/docs/scripts/frame/iife/index.min.js +++ b/docs/scripts/frame/iife/index.min.js @@ -1 +1 @@ -var fledgeframe=function(){"use strict";let e=[];const o={},n="%c ";function t(t){return function(...r){const s=[],a=[];r.forEach((t=>{if(t===o){const o=e.shift();s.push(`%c${o.value}`,n),a.push(o.css,"")}else"object"==typeof t||"function"==typeof t?(s.push("%o",n),a.push(t,"")):(s.push(`%c${t}`,n),a.push("",""))})),t(s.join(""),...a),e=[]}}const r={assert:t(console.assert),clear:t(console.clear),count:t(console.count),countReset:t(console.countReset),debug:t(console.debug),dir:t(console.dir),error:t(console.error),group:t(console.group),groupCollapsed:t(console.groupCollapsed),groupEnd:t(console.groupEnd),info:t(console.info),log:t(console.log),table:t(console.table),time:t(console.time),timeEnd:t(console.timeEnd),timeLog:t(console.timeLog),trace:t(console.trace),warn:t(console.warn),asAlert:function(n){return e.push({value:n,css:"display: inline-block; background-color: #dc3545; color: #ffffff; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),o},asInfo:function(n){return e.push({value:n,css:"color: #0366d6; font-weight: bold;"}),o},asProcess:function(n){return e.push({value:`${n}…`,css:"color: #8c8c8c; font-style: italic;"}),o},asSuccess:function(n){return e.push({value:n,css:"color: #289d45; font-weight: bold;"}),o},asWarning:function(n){return e.push({value:n,css:"display: inline-block; background-color: #ffc107; color: black; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),o}};function s(e){return new Promise(((o,n)=>{e.oncomplete=e.onsuccess=()=>o(e.result),e.onabort=e.onerror=()=>n(e.error)}))}let a;function i(){return a||(a=function(e,o){const n=indexedDB.open(e);n.onupgradeneeded=()=>n.result.createObjectStore(o);const t=s(n);return(e,n)=>t.then((t=>n(t.transaction(o,e).objectStore(o))))}("keyval-store","keyval")),a}function c(e=i()){const o=[];return function(e,o){return e("readonly",(e=>(e.openCursor().onsuccess=function(){this.result&&(o(this.result),this.result.continue())},s(e.transaction))))}(e,(e=>o.push([e.key,e.value]))).then((()=>o))}const l=async(e,o,n)=>{n&&r.groupCollapsed("auction utils: getTrustedSignals");const t=`hostname=${window.top.location.hostname}`;if(!e||!o)return n&&r.log(r.asWarning("No 'url' or 'keys' found!")),void(n&&r.groupEnd());const s=await fetch(`${e}?${t}&keys=${o.join(",")}`).then((e=>{if(!e.ok)throw new Error("Something went wrong! The response returned was not ok.");if(!(e=>/\bapplication\/json\b/.test(e?.headers?.get("content-type")))(e))throw new Error("Response was not in the format of JSON.");return e.json()})).catch((e=>(n&&r.log(r.asAlert("There was a problem with your fetch operation:")),n&&r.log(e),null))),a={};for(const[e,n]of s)o.includes(e)&&(a[e]=n);return a&&0===Object.keys(a).length&&a.constructor===Object?(n&&r.groupEnd(),a):(n&&r.log(r.asWarning("No signals found!")),n&&r.groupEnd(),null)};async function u(e,o){o&&r.groupCollapsed("Fledge API: runAdAuction");const n=await c();o&&r.log(r.asInfo("all interest groups:"),n);const t=((e,o,n)=>{if(n&&r.groupCollapsed("auction utils: getEligible"),"*"===o)return n&&r.info("using the wildcard yields all groups"),n&&r.groupEnd(),e;const t=e.filter((([e,n])=>o.includes(n.owner)));return t.length?(n&&r.info("found some eligible buyers"),n&&r.groupEnd(),t):(n&&r.log(r.asWarning("No groups were eligible!")),n&&r.groupEnd(),null)})(n,e.interest_group_buyers,o);if(o&&r.log(r.asInfo('eligible buyers based on "interest_group_buyers":'),t),!t)return o&&r.log(r.asAlert("No eligible interest group buyers found!")),null;const s=await(async(e,o,n)=>Promise.all(e.map((async([e,t])=>{n&&r.groupCollapsed(`auction utils: getBids => ${e}`);const s=performance.now(),{generateBid:a,generate_bid:i}=await import(t.bidding_logic_url);let c=a;if(i&&!a&&(c=i),!c&&"function"!=typeof c)return n&&r.log(r.asWarning("No 'generateBid' function found!")),n&&r.groupEnd(),null;const u=await l(t?.trusted_bidding_signals_url,t?.trusted_bidding_signals_keys,n);let g;try{g=c(t,o?.auction_signals,o?.per_buyer_signals?.[t.owner],u,{top_window_hostname:window.top.location.hostname,seller:o.seller}),n&&r.log(r.asInfo("bid:"),g)}catch(e){return n&&r.log(r.asAlert("There was an error in the 'generateBid' function:")),n&&r.log(e),null}if(!(g.ad&&"object"==typeof g.ad&&g.bid&&"number"==typeof g.bid&&g.render)||"string"!=typeof g.render&&!Array.isArray(g.render))return n&&r.log(r.asWarning("No bid found!")),n&&r.groupEnd(),null;const d=performance.now();return n&&r.groupEnd(),{...t,...g,duration:d-s}}))))(t,e,o);o&&r.log(r.asInfo("all bids from each buyer:"),s);const a=s.filter((e=>e));if(o&&r.log(r.asInfo("filtered bids:"),a),!a.length)return o&&r.log(r.asAlert("No bids found!")),o&&r.groupEnd(),null;o&&r.log(r.asProcess("getting all scores, filtering and sorting"));const[i]=await(async(e,o,n)=>{n&&r.groupCollapsed("auction utils: getScores");const{scoreAd:t,score_ad:s}=await import(o.decision_logic_url);let a=t;return s&&!t&&(a=s),a||"function"==typeof a?e.map((e=>{let t;try{t=a(e?.ad,e?.bid,o,o?.trusted_scoring_signals,{top_window_hostname:window.top.location.hostname,interest_group_owner:e.owner,interest_group_name:e.name,bidding_duration_msec:e.duration}),n&&r.log(r.asInfo("score:"),t)}catch(e){n&&r.log(r.asAlert("There was an error in the 'scoreAd' function:")),n&&r.log(e),t=-1}return n&&r.groupEnd(),{bid:e,score:t}})).filter((({score:e})=>e>0)).sort(((e,o)=>e.score>o.score?1:-1)):(n&&r.log(r.asWarning("No 'scoreAd' function was found!")),null)})(a,e,o);if(o&&r.log(r.asInfo("winner:"),i),!i)return o&&r.log(r.asAlert("No winner found!")),o&&r.groupEnd(),null;o&&r.log(r.asProcess("creating an entry in the auction store"));const u=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(e=>(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)));return sessionStorage.setItem(u,JSON.stringify({origin:`${window.top.location.origin}${window.top.location.pathname}`,timestamp:Date.now(),conf:e,...i})),o&&r.log(r.asSuccess("auction token:"),u),o&&r.groupEnd(),u}const g=(e,o)=>`${e}-${o}`;async function d(e,o,n){n&&r.groupCollapsed("Fledge API: joinAdInterest");const t=g(e.owner,e.name),a=await function(e,o=i()){return o("readonly",(o=>s(o.get(e))))}(t);return n&&r.log(r.asInfo("checking for an existing interest group:"),a),a?(n&&r.log(r.asProcess("updating an interest group")),await function(e,o,n=i()){return n("readwrite",(n=>new Promise(((t,r)=>{n.get(e).onsuccess=function(){try{n.put(o(this.result),e),t(s(n.transaction))}catch(e){r(e)}}}))))}(t,{_expired:Date.now()+o,...e})):(n&&r.log(r.asProcess("creating a new interest group")),await function(e,o,n=i()){return n("readwrite",(n=>(n.put(o,e),s(n.transaction))))}(t,{_created:Date.now(),_expired:Date.now()+o,_updated:Date.now(),...e})),n&&r.log(r.asSuccess("interest group id:"),t),n&&r.groupEnd(),!0}async function p(e,o){return o&&r.groupCollapsed("Fledge API: leaveAdInterest"),o&&r.log(r.asProcess("deleting an existing interest group")),await function(e,o=i()){return o("readwrite",(o=>(o.delete(e),s(o.transaction))))}(g(e.owner,e.name)),o&&r.log(r.asSuccess("interest group deleted")),o&&r.groupEnd(),!0}async function f({data:e,ports:o}){try{if(!Array.isArray(e))throw new Error(`The API expects the data to be in the form of an array, with index 0 set to the action, and index 1 set to the data. A ${typeof e} was passed instead.`);switch(e[0]){case"joinAdInterestGroup":{const[,o]=e,[n,t,r]=o;return await d(n,t,r),!0}case"leaveAdInterestGroup":{const[,o]=e,[n,t]=o;return await p(n,t),!0}case"runAdAuction":{const[,n]=e,[t,r]=n;if(1!==o.length)throw new Error(`Port transfer mismatch during request: expected 1 port, but received ${o.length}`);const[s]=o,a=[!0,await u(t,r)];return s.postMessage(a),s.close(),!0}default:return!1}}catch(e){const n=[!1];for(const e of o)e.postMessage(n);throw e}}return async function(){const{searchParams:e}=new URL(window.location),o=e.get("debug")||!1;if(o&&r.group("Fledge: Storage Frame"),!(e.get("admin")||!1)){const[e]=window.location.ancestorOrigins;if(void 0===e)throw o&&r.log(r.asWarning("It appears your attempting to access this from the top-level document")),o&&r.log({origin:e,location:window.location}),new Error("Can't call 'postMessage' on the Frame window when run as a top-level document");const{port1:n,port2:t}=new MessageChannel;o&&r.log("message channel receiver:",n),o&&r.log("message channel sender:",t),n.onmessage=f,window.parent.postMessage({"fledge.polyfill":1},e,[t])}o&&r.groupEnd()}}(); +var fledgeframe=function(){"use strict";let e=[];const o={},n="%c ";function t(t){return function(...r){const s=[],a=[];r.forEach((t=>{if(t===o){const o=e.shift();s.push(`%c${o.value}`,n),a.push(o.css,"")}else"object"==typeof t||"function"==typeof t?(s.push("%o",n),a.push(t,"")):(s.push(`%c${t}`,n),a.push("",""))})),t(s.join(""),...a),e=[]}}const r={assert:t(console.assert),clear:t(console.clear),count:t(console.count),countReset:t(console.countReset),debug:t(console.debug),dir:t(console.dir),error:t(console.error),group:t(console.group),groupCollapsed:t(console.groupCollapsed),groupEnd:t(console.groupEnd),info:t(console.info),log:t(console.log),table:t(console.table),time:t(console.time),timeEnd:t(console.timeEnd),timeLog:t(console.timeLog),trace:t(console.trace),warn:t(console.warn),asAlert:function(n){return e.push({value:n,css:"display: inline-block; background-color: #dc3545; color: #ffffff; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),o},asInfo:function(n){return e.push({value:n,css:"color: #0366d6; font-weight: bold;"}),o},asProcess:function(n){return e.push({value:`${n}…`,css:"color: #8c8c8c; font-style: italic;"}),o},asSuccess:function(n){return e.push({value:n,css:"color: #289d45; font-weight: bold;"}),o},asWarning:function(n){return e.push({value:n,css:"display: inline-block; background-color: #ffc107; color: black; font-weight: bold; padding: 3px 7px 3px 7px; border-radius: 3px 3px 3px 3px;"}),o}};function s(e){return new Promise(((o,n)=>{e.oncomplete=e.onsuccess=()=>o(e.result),e.onabort=e.onerror=()=>n(e.error)}))}function a(e,o){const n=indexedDB.open(e);n.onupgradeneeded=()=>n.result.createObjectStore(o);const t=s(n);return(e,n)=>t.then((t=>n(t.transaction(o,e).objectStore(o))))}let i;function c(){return i||(i=a("keyval-store","keyval")),i}function l(e=c()){const o=[];return function(e,o){return e("readonly",(e=>(e.openCursor().onsuccess=function(){this.result&&(o(this.result),this.result.continue())},s(e.transaction))))}(e,(e=>o.push([e.key,e.value]))).then((()=>o))}const u=a("fledge.v1","interest-groups"),g=(e,o)=>`${e}-${o}`;async function d(e,o,n){n&&r.groupCollapsed("Fledge API: joinAdInterest");const t=g(e.owner,e.name),a=await function(e,o=c()){return o("readonly",(o=>s(o.get(e))))}(t,u);return n&&r.log(r.asInfo("checking for an existing interest group:"),a),a?(n&&r.log(r.asProcess("updating an interest group")),await function(e,o,n=c()){return n("readwrite",(n=>new Promise(((t,r)=>{n.get(e).onsuccess=function(){try{n.put(o(this.result),e),t(s(n.transaction))}catch(e){r(e)}}}))))}(t,{_expired:Date.now()+o,...e},u)):(n&&r.log(r.asProcess("creating a new interest group")),await function(e,o,n=c()){return n("readwrite",(n=>(n.put(o,e),s(n.transaction))))}(t,{_created:Date.now(),_expired:Date.now()+o,_updated:Date.now(),...e},u)),n&&r.log(r.asSuccess("interest group id:"),t),n&&r.groupEnd(),!0}async function p(e,o){return o&&r.groupCollapsed("Fledge API: leaveAdInterest"),o&&r.log(r.asProcess("deleting an existing interest group")),await function(e,o=c()){return o("readwrite",(o=>(o.delete(e),s(o.transaction))))}(g(e.owner,e.name),u),o&&r.log(r.asSuccess("interest group deleted")),o&&r.groupEnd(),!0}const f=async(e,o,n)=>{n&&r.groupCollapsed("auction utils: getTrustedSignals");const t=`hostname=${window.top.location.hostname}`;if(!e||!o)return n&&r.log(r.asWarning("No 'url' or 'keys' found!")),void(n&&r.groupEnd());const s=await fetch(`${e}?${t}&keys=${o.join(",")}`).then((e=>{if(!e.ok)throw new Error("Something went wrong! The response returned was not ok.");if(!(e=>/\bapplication\/json\b/.test(e?.headers?.get("content-type")))(e))throw new Error("Response was not in the format of JSON.");return e.json()})).catch((e=>(n&&r.log(r.asAlert("There was a problem with your fetch operation:")),n&&r.log(e),null))),a={};for(const[e,n]of s)o.includes(e)&&(a[e]=n);return a&&0===Object.keys(a).length&&a.constructor===Object?(n&&r.groupEnd(),a):(n&&r.log(r.asWarning("No signals found!")),n&&r.groupEnd(),null)};async function w(e,o){o&&r.groupCollapsed("Fledge API: runAdAuction");const n=await l(u);o&&r.log(r.asInfo("all interest groups:"),n);const t=((e,o,n)=>{if(n&&r.groupCollapsed("auction utils: getEligible"),"*"===o)return n&&r.info("using the wildcard yields all groups"),n&&r.groupEnd(),e;const t=e.filter((([e,n])=>o.includes(n.owner)));return t.length?(n&&r.info("found some eligible buyers"),n&&r.groupEnd(),t):(n&&r.log(r.asWarning("No groups were eligible!")),n&&r.groupEnd(),null)})(n,e.interest_group_buyers,o);if(o&&r.log(r.asInfo('eligible buyers based on "interest_group_buyers":'),t),!t)return o&&r.log(r.asAlert("No eligible interest group buyers found!")),null;const s=await(async(e,o,n)=>Promise.all(e.map((async([e,t])=>{n&&r.groupCollapsed(`auction utils: getBids => ${e}`);const s=performance.now(),{generateBid:a,generate_bid:i}=await import(t.bidding_logic_url);let c=a;if(i&&!a&&(c=i),!c&&"function"!=typeof c)return n&&r.log(r.asWarning("No 'generateBid' function found!")),n&&r.groupEnd(),null;const l=await f(t?.trusted_bidding_signals_url,t?.trusted_bidding_signals_keys,n);let u;try{u=c(t,o?.auction_signals,o?.per_buyer_signals?.[t.owner],l,{top_window_hostname:window.top.location.hostname,seller:o.seller}),n&&r.log(r.asInfo("bid:"),u)}catch(e){return n&&r.log(r.asAlert("There was an error in the 'generateBid' function:")),n&&r.log(e),null}if(!(u.ad&&"object"==typeof u.ad&&u.bid&&"number"==typeof u.bid&&u.render)||"string"!=typeof u.render&&!Array.isArray(u.render))return n&&r.log(r.asWarning("No bid found!")),n&&r.groupEnd(),null;const g=performance.now();return n&&r.groupEnd(),{...t,...u,duration:g-s}}))))(t,e,o);o&&r.log(r.asInfo("all bids from each buyer:"),s);const a=s.filter((e=>e));if(o&&r.log(r.asInfo("filtered bids:"),a),!a.length)return o&&r.log(r.asAlert("No bids found!")),o&&r.groupEnd(),null;o&&r.log(r.asProcess("getting all scores, filtering and sorting"));const[i]=await(async(e,o,n)=>{n&&r.groupCollapsed("auction utils: getScores");const{scoreAd:t,score_ad:s}=await import(o.decision_logic_url);let a=t;return s&&!t&&(a=s),a||"function"==typeof a?e.map((e=>{let t;try{t=a(e?.ad,e?.bid,o,o?.trusted_scoring_signals,{top_window_hostname:window.top.location.hostname,interest_group_owner:e.owner,interest_group_name:e.name,bidding_duration_msec:e.duration}),n&&r.log(r.asInfo("score:"),t)}catch(e){n&&r.log(r.asAlert("There was an error in the 'scoreAd' function:")),n&&r.log(e),t=-1}return n&&r.groupEnd(),{bid:e,score:t}})).filter((({score:e})=>e>0)).sort(((e,o)=>e.score>o.score?1:-1)):(n&&r.log(r.asWarning("No 'scoreAd' function was found!")),null)})(a,e,o);if(o&&r.log(r.asInfo("winner:"),i),!i)return o&&r.log(r.asAlert("No winner found!")),o&&r.groupEnd(),null;o&&r.log(r.asProcess("creating an entry in the auction store"));const c=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(e=>(e^crypto.getRandomValues(new Uint8Array(1))[0]&15>>e/4).toString(16)));return sessionStorage.setItem(c,JSON.stringify({origin:`${window.top.location.origin}${window.top.location.pathname}`,timestamp:Date.now(),conf:e,...i})),o&&r.log(r.asSuccess("auction token:"),c),o&&r.groupEnd(),c}async function h({data:e,ports:o}){try{if(!Array.isArray(e))throw new Error(`The API expects the data to be in the form of an array, with index 0 set to the action, and index 1 set to the data. A ${typeof e} was passed instead.`);switch(e[0]){case"joinAdInterestGroup":{const[,o]=e,[n,t,r]=o;return await d(n,t,r),!0}case"leaveAdInterestGroup":{const[,o]=e,[n,t]=o;return await p(n,t),!0}case"runAdAuction":{const[,n]=e,[t,r]=n;if(1!==o.length)throw new Error(`Port transfer mismatch during request: expected 1 port, but received ${o.length}`);const[s]=o,a=[!0,await w(t,r)];return s.postMessage(a),s.close(),!0}default:return!1}}catch(e){const n=[!1];for(const e of o)e.postMessage(n);throw e}}return async function(){const{searchParams:e}=new URL(window.location),o=e.get("debug")||!1;if(o&&r.group("Fledge: Storage Frame"),!(e.get("admin")||!1)){const[e]=window.location.ancestorOrigins;if(void 0===e)throw o&&r.log(r.asWarning("It appears your attempting to access this from the top-level document")),o&&r.log({origin:e,location:window.location}),new Error("Can't call 'postMessage' on the Frame window when run as a top-level document");const{port1:n,port2:t}=new MessageChannel;o&&r.log("message channel receiver:",n),o&&r.log("message channel sender:",t),n.onmessage=h,window.parent.postMessage({"fledge.polyfill":1},e,[t])}o&&r.groupEnd()}}(); From 8ff8fde77f6bc70ac8ff1546140c57553affd506 Mon Sep 17 00:00:00 2001 From: Newton Koumantzelis Date: Thu, 13 May 2021 12:08:50 -0700 Subject: [PATCH 5/5] chore: clean up e2e test with server --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3ac9e88..34818c3 100644 --- a/package.json +++ b/package.json @@ -93,14 +93,14 @@ "scripts": { "audit": "bundlesize --config ./node_modules/@theholocron/bundlewatch-config/index.js", "commit": "commit", - "predist:docs": "rollup -c node:@theholocron/rollup-config --input=src/frame --output=docs/scripts/frame --name=fledgeframe --exports=default", - "dist:docs": "rollup -c node:@theholocron/rollup-config --input=src/api --output=docs/scripts/api --name=fledge --exports=default", "predist": "rollup -c node:@theholocron/rollup-config --input=src/frame --output=dist/frame --name=fledgeframe --exports=default", "dist": "rollup -c node:@theholocron/rollup-config --input=src/api --output=dist/api --name=fledge --exports=default", + "predocs": "rollup -c node:@theholocron/rollup-config --input=src/frame --output=docs/scripts/frame --name=fledgeframe --exports=default --watch", + "docs": "rollup -c node:@theholocron/rollup-config --input=src/api --output=docs/scripts/api --name=fledge --exports=default --watch", "lint": "eslint .", - "preserve": "npm run dist:docs", - "serve": "serve . -l 3000", "release": "semantic-release", + "serve": "serve . -l 3000", + "start": "npm run serve", "test": "npm run test:docs && npm run test:unit && npm run test:e2e", "test:docs": "alex .", "pretest:e2e": "npm run dist",