vendor: 9,970 bytes, lines 1-330
1function assertNonEmptyString (str) { 2 if (typeof str !== 'string' || !str) { 3 throw new Error('expected a non-empty string, got: ' + str) 4 } 5} 6 7function assertNumber (number) { 8 if (typeof number !== 'number') { 9 throw new Error('expected a number, got: ' + number) 10 } 11} 12 13const DB_VERSION_CURRENT = 1; 14const DB_VERSION_INITIAL = 1; 15const STORE_EMOJI = 'emoji'; 16const STORE_KEYVALUE = 'keyvalue'; 17const STORE_FAVORITES = 'favorites'; 18const FIELD_TOKENS = 'tokens'; 19const INDEX_TOKENS = 'tokens'; 20const FIELD_UNICODE = 'unicode'; 21const INDEX_COUNT = 'count'; 22const FIELD_GROUP = 'group'; 23const FIELD_ORDER = 'order'; 24const INDEX_GROUP_AND_ORDER = 'group-order'; 25const KEY_ETAG = 'eTag'; 26const KEY_URL = 'url'; 27const KEY_PREFERRED_SKINTONE = 'skinTone'; 28const MODE_READONLY = 'readonly'; 29const MODE_READWRITE = 'readwrite'; 30const INDEX_SKIN_UNICODE = 'skinUnicodes'; 31const FIELD_SKIN_UNICODE = 'skinUnicodes'; 32 33const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json'; 34const DEFAULT_LOCALE = 'en'; 35 36// like lodash's uniqBy but much smaller 37function uniqBy (arr, func) { 38 const set = new Set(); 39 const res = []; 40 for (const item of arr) { 41 const key = func(item); 42 if (!set.has(key)) { 43 set.add(key); 44 res.push(item); 45 } 46 } 47 return res 48} 49 50function uniqEmoji (emojis) { 51 return uniqBy(emojis, _ => _.unicode) 52} 53 54function initialMigration (db) { 55 function createObjectStore (name, keyPath, indexes) { 56 const store = keyPath 57 ? db.createObjectStore(name, { keyPath }) 58 : db.createObjectStore(name); 59 if (indexes) { 60 for (const [indexName, [keyPath, multiEntry]] of Object.entries(indexes)) { 61 store.createIndex(indexName, keyPath, { multiEntry }); 62 } 63 } 64 return store 65 } 66 67 createObjectStore(STORE_KEYVALUE); 68 createObjectStore(STORE_EMOJI, /* keyPath */ FIELD_UNICODE, { 69 [INDEX_TOKENS]: [FIELD_TOKENS, /* multiEntry */ true], 70 [INDEX_GROUP_AND_ORDER]: [[FIELD_GROUP, FIELD_ORDER]], 71 [INDEX_SKIN_UNICODE]: [FIELD_SKIN_UNICODE, /* multiEntry */ true] 72 }); 73 createObjectStore(STORE_FAVORITES, undefined, { 74 [INDEX_COUNT]: [''] 75 }); 76} 77 78const openIndexedDBRequests = {}; 79const databaseCache = {}; 80const onCloseListeners = {}; 81 82function handleOpenOrDeleteReq (resolve, reject, req) { 83 // These things are almost impossible to test with fakeIndexedDB sadly 84 /* istanbul ignore next */ 85 req.onerror = () => reject(req.error); 86 /* istanbul ignore next */ 87 req.onblocked = () => reject(new Error('IDB blocked')); 88 req.onsuccess = () => resolve(req.result); 89} 90 91async function createDatabase (dbName) { 92 const db = await new Promise((resolve, reject) => { 93 const req = indexedDB.open(dbName, DB_VERSION_CURRENT); 94 openIndexedDBRequests[dbName] = req; 95 req.onupgradeneeded = e => { 96 // Technically there is only one version, so we don't need this `if` check 97 // But if an old version of the JS is in another browser tab 98 // and it gets upgraded in the future and we have a new DB version, well... 99 // better safe than sorry. 100 /* istanbul ignore else */ 101 if (e.oldVersion < DB_VERSION_INITIAL) { 102 initialMigration(req.result); 103 } 104 }; 105 handleOpenOrDeleteReq(resolve, reject, req); 106 }); 107 // Handle abnormal closes, e.g. "delete database" in chrome dev tools. 108 // No need for removeEventListener, because once the DB can no longer 109 // fire "close" events, it will auto-GC. 110 // Unfortunately cannot test in fakeIndexedDB: https://github.com/dumbmatter/fakeIndexedDB/issues/50 111 /* istanbul ignore next */ 112 db.onclose = () => closeDatabase(dbName); 113 return db 114} 115 116function openDatabase (dbName) { 117 if (!databaseCache[dbName]) { 118 databaseCache[dbName] = createDatabase(dbName); 119 } 120 return databaseCache[dbName] 121} 122 123function dbPromise (db, storeName, readOnlyOrReadWrite, cb) { 124 return new Promise((resolve, reject) => { 125 // Use relaxed durability because neither the emoji data nor the favorites/preferred skin tone 126 // are really irreplaceable data. IndexedDB is just a cache in this case. 127 const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: 'relaxed' }); 128 const store = typeof storeName === 'string' 129 ? txn.objectStore(storeName) 130 : storeName.map(name => txn.objectStore(name)); 131 let res; 132 cb(store, txn, (result) => { 133 res = result; 134 }); 135 136 txn.oncomplete = () => resolve(res); 137 /* istanbul ignore next */ 138 txn.onerror = () => reject(txn.error); 139 }) 140} 141 142function closeDatabase (dbName) { 143 // close any open requests 144 const req = openIndexedDBRequests[dbName]; 145 const db = req && req.result; 146 if (db) { 147 db.close(); 148 const listeners = onCloseListeners[dbName]; 149 /* istanbul ignore else */ 150 if (listeners) { 151 for (const listener of listeners) { 152 listener(); 153 } 154 } 155 } 156 delete openIndexedDBRequests[dbName]; 157 delete databaseCache[dbName]; 158 delete onCloseListeners[dbName]; 159} 160 161function deleteDatabase (dbName) { 162 return new Promise((resolve, reject) => { 163 // close any open requests 164 closeDatabase(dbName); 165 const req = indexedDB.deleteDatabase(dbName); 166 handleOpenOrDeleteReq(resolve, reject, req); 167 }) 168} 169 170// The "close" event occurs during an abnormal shutdown, e.g. a user clearing their browser data. 171// However, it doesn't occur with the normal "close" event, so we handle that separately. 172// https://www.w3.org/TR/IndexedDB/#close-a-database-connection 173function addOnCloseListener (dbName, listener) { 174 let listeners = onCloseListeners[dbName]; 175 if (!listeners) { 176 listeners = onCloseListeners[dbName] = []; 177 } 178 listeners.push(listener); 179} 180 181// list of emoticons that don't match a simple \W+ regex 182// extracted using: 183// require('emoji-picker-element-data/en/emojibase/data.json').map(_ => _.emoticon).filter(Boolean).filter(_ => !/^\W+$/.test(_)) 184const irregularEmoticons = new Set([ 185 ':D', 'XD', ":'D", 'O:)', 186 ':X', ':P', ';P', 'XP', 187 ':L', ':Z', ':j', '8D', 188 'XO', '8)', ':B', ':O', 189 ':S', ":'o", 'Dx', 'X(', 190 'D:', ':C', '>0)', ':3', 191 '</3', '<3', '\\M/', ':E', 192 '8#' 193]); 194 195function extractTokens (str) { 196 return str 197 .split(/[\s_]+/) 198 .map(word => { 199 if (!word.match(/\w/) || irregularEmoticons.has(word)) { 200 // for pure emoticons like :) or :-), just leave them as-is 201 return word.toLowerCase() 202 } 203 204 return word 205 .replace(/[)(:,]/g, '') 206 .replace(/â/g, "'") 207 .toLowerCase() 208 }).filter(Boolean) 209} 210 211const MIN_SEARCH_TEXT_LENGTH = 2; 212 213// This is an extra step in addition to extractTokens(). The difference here is that we expect 214// the input to have already been run through extractTokens(). This is useful for cases like 215// emoticons, where we don't want to do any tokenization (because it makes no sense to split up 216// ">:)" by the colon) but we do want to lowercase it to have consistent search results, so that 217// the user can type ':P' or ':p' and still get the same result. 218function normalizeTokens (str) { 219 return str 220 .filter(Boolean) 221 .map(_ => _.toLowerCase()) 222 .filter(_ => _.length >= MIN_SEARCH_TEXT_LENGTH) 223} 224 225// Transform emoji data for storage in IDB 226function transformEmojiData (emojiData) { 227 const res = emojiData.map(({ annotation, emoticon, group, order, shortcodes, skins, tags, emoji, version }) => { 228 const tokens = [...new Set( 229 normalizeTokens([ 230 ...(shortcodes || []).map(extractTokens).flat(), 231 ...tags.map(extractTokens).flat(), 232 ...extractTokens(annotation), 233 emoticon 234 ]) 235 )].sort(); 236 const res = { 237 annotation, 238 group, 239 order, 240 tags, 241 tokens, 242 unicode: emoji, 243 version 244 }; 245 if (emoticon) { 246 res.emoticon = emoticon; 247 } 248 if (shortcodes) { 249 res.shortcodes = shortcodes; 250 } 251 if (skins) { 252 res.skinTones = []; 253 res.skinUnicodes = []; 254 res.skinVersions = []; 255 for (const { tone, emoji, version } of skins) { 256 res.skinTones.push(tone); 257 res.skinUnicodes.push(emoji); 258 res.skinVersions.push(version); 259 } 260 } 261 return res 262 }); 263 return res 264} 265 266// helper functions that help compress the code better 267 268function callStore (store, method, key, cb) { 269 store[method](key).onsuccess = e => (cb && cb(e.target.result)); 270} 271 272function getIDB (store, key, cb) { 273 callStore(store, 'get', key, cb); 274} 275 276function getAllIDB (store, key, cb) { 277 callStore(store, 'getAll', key, cb); 278} 279 280function commit (txn) { 281 /* istanbul ignore else */ 282 if (txn.commit) { 283 txn.commit(); 284 } 285} 286 287// like lodash's minBy 288function minBy (array, func) { 289 let minItem = array[0]; 290 for (let i = 1; i < array.length; i++) { 291 const item = array[i]; 292 if (func(minItem) > func(item)) { 293 minItem = item; 294 } 295 } 296 return minItem 297} 298 299// return an array of results representing all items that are found in each one of the arrays 300 301function findCommonMembers (arrays, uniqByFunc) { 302 const shortestArray = minBy(arrays, _ => _.length); 303 const results = []; 304 for (const item of shortestArray) { 305 // if this item is included in every array in the intermediate results, add it to the final results 306 if (!arrays.some(array => array.findIndex(_ => uniqByFunc(_) === uniqByFunc(item)) === -1)) { 307 results.push(item); 308 } 309 } 310 return results 311} 312 313async function isEmpty (db) { 314 return !(await get(db, STORE_KEYVALUE, KEY_URL)) 315} 316 317async function hasData (db, url, eTag) { 318 const [oldETag, oldUrl] = await Promise.all([KEY_ETAG, KEY_URL] 319 .map(key => get(db, STORE_KEYVALUE, key))); 320 return (oldETag === eTag && oldUrl === url) 321} 322 323async function doFullDatabaseScanForSingleResult (db, predicate) { 324 // This batching algorithm is just a perf improvement over a basic 325 // cursor. The BATCH_SIZE is an estimate of what would give the best 326 // perf for doing a full DB scan (worst case). 327 // 328 // Mini-benchmark for determining the best batch size: 329 // 330 // PERF=1 yarn build:rollup && yarn
vendor: 9,042 bytes, lines 330-633
330 test:adhoc 331 // 332 // (async () => { 333 // performance.mark('start') 334 // await $('emoji-picker').database.getEmojiByShortcode('doesnotexist') 335 // performance.measure('total', 'start') 336 // console.log(performance.getEntriesByName('total').slice(-1)[0].duration) 337 // })() 338 const BATCH_SIZE = 50; // Typically around 150ms for 6x slowdown in Chrome for above benchmark 339 return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { 340 let lastKey; 341 342 const processNextBatch = () => { 343 emojiStore.getAll(lastKey && IDBKeyRange.lowerBound(lastKey, true), BATCH_SIZE).onsuccess = e => { 344 const results = e.target.result; 345 for (const result of results) { 346 lastKey = result.unicode; 347 if (predicate(result)) { 348 return cb(result) 349 } 350 } 351 if (results.length < BATCH_SIZE) { 352 return cb() 353 } 354 processNextBatch(); 355 }; 356 }; 357 processNextBatch(); 358 }) 359} 360 361async function loadData (db, emojiData, url, eTag) { 362 try { 363 const transformedData = transformEmojiData(emojiData); 364 await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => { 365 let oldETag; 366 let oldUrl; 367 let todo = 0; 368 369 function checkFetched () { 370 if (++todo === 2) { // 2 requests made 371 onFetched(); 372 } 373 } 374 375 function onFetched () { 376 if (oldETag === eTag && oldUrl === url) { 377 // check again within the transaction to guard against concurrency, e.g. multiple browser tabs 378 return 379 } 380 // delete old data 381 emojiStore.clear(); 382 // insert new data 383 for (const data of transformedData) { 384 emojiStore.put(data); 385 } 386 metaStore.put(eTag, KEY_ETAG); 387 metaStore.put(url, KEY_URL); 388 commit(txn); 389 } 390 391 getIDB(metaStore, KEY_ETAG, result => { 392 oldETag = result; 393 checkFetched(); 394 }); 395 396 getIDB(metaStore, KEY_URL, result => { 397 oldUrl = result; 398 checkFetched(); 399 }); 400 }); 401 } finally { 402 } 403} 404 405async function getEmojiByGroup (db, group) { 406 return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { 407 const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true); 408 getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb); 409 }) 410} 411 412async function getEmojiBySearchQuery (db, query) { 413 const tokens = normalizeTokens(extractTokens(query)); 414 415 if (!tokens.length) { 416 return [] 417 } 418 419 return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => { 420 // get all results that contain all tokens (i.e. an AND query) 421 const intermediateResults = []; 422 423 const checkDone = () => { 424 if (intermediateResults.length === tokens.length) { 425 onDone(); 426 } 427 }; 428 429 const onDone = () => { 430 const results = findCommonMembers(intermediateResults, _ => _.unicode); 431 cb(results.sort((a, b) => a.order < b.order ? -1 : 1)); 432 }; 433 434 for (let i = 0; i < tokens.length; i++) { 435 const token = tokens[i]; 436 const range = i === tokens.length - 1 437 ? IDBKeyRange.bound(token, token + '\uffff', false, true) // treat last token as a prefix search 438 : IDBKeyRange.only(token); // treat all other tokens as an exact match 439 getAllIDB(emojiStore.index(INDEX_TOKENS), range, result => { 440 intermediateResults.push(result); 441 checkDone(); 442 }); 443 } 444 }) 445} 446 447// This could have been implemented as an IDB index on shortcodes, but it seemed wasteful to do that 448// when we can already query by tokens and this will give us what we're looking for 99.9% of the time 449async function getEmojiByShortcode (db, shortcode) { 450 const emojis = await getEmojiBySearchQuery(db, shortcode); 451 452 // In very rare cases (e.g. the shortcode "v" as in "v for victory"), we cannot search because 453 // there are no usable tokens (too short in this case). In that case, we have to do an inefficient 454 // full-database scan, which I believe is an acceptable tradeoff for not having to have an extra 455 // index on shortcodes. 456 457 if (!emojis.length) { 458 const predicate = _ => ((_.shortcodes || []).includes(shortcode.toLowerCase())); 459 return (await doFullDatabaseScanForSingleResult(db, predicate)) || null 460 } 461 462 return emojis.filter(_ => { 463 const lowerShortcodes = (_.shortcodes || []).map(_ => _.toLowerCase()); 464 return lowerShortcodes.includes(shortcode.toLowerCase()) 465 })[0] || null 466} 467 468async function getEmojiByUnicode (db, unicode) { 469 return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => ( 470 getIDB(emojiStore, unicode, result => { 471 if (result) { 472 return cb(result) 473 } 474 getIDB(emojiStore.index(INDEX_SKIN_UNICODE), unicode, result => cb(result || null)); 475 }) 476 )) 477} 478 479function get (db, storeName, key) { 480 return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => ( 481 getIDB(store, key, cb) 482 )) 483} 484 485function set (db, storeName, key, value) { 486 return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => { 487 store.put(value, key); 488 commit(txn); 489 }) 490} 491 492function incrementFavoriteEmojiCount (db, unicode) { 493 return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => ( 494 getIDB(store, unicode, result => { 495 store.put((result || 0) + 1, unicode); 496 commit(txn); 497 }) 498 )) 499} 500 501function getTopFavoriteEmoji (db, customEmojiIndex, limit) { 502 if (limit === 0) { 503 return [] 504 } 505 return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => { 506 const results = []; 507 favoritesStore.index(INDEX_COUNT).openCursor(undefined, 'prev').onsuccess = e => { 508 const cursor = e.target.result; 509 if (!cursor) { // no more results 510 return cb(results) 511 } 512 513 function addResult (result) { 514 results.push(result); 515 if (results.length === limit) { 516 return cb(results) // done, reached the limit 517 } 518 cursor.continue(); 519 } 520 521 const unicodeOrName = cursor.primaryKey; 522 const custom = customEmojiIndex.byName(unicodeOrName); 523 if (custom) { 524 return addResult(custom) 525 } 526 // This could be done in parallel (i.e. make the cursor and the get()s parallelized), 527 // but my testing suggests it's not actually faster. 528 getIDB(emojiStore, unicodeOrName, emoji => { 529 if (emoji) { 530 return addResult(emoji) 531 } 532 // emoji not found somehow, ignore (may happen if custom emoji change) 533 cursor.continue(); 534 }); 535 }; 536 }) 537} 538 539// trie data structure for prefix searches 540// loosely based on https://github.com/nolanlawson/substring-trie 541 542const CODA_MARKER = ''; // marks the end of the string 543 544function trie (arr, itemToTokens) { 545 const map = new Map(); 546 for (const item of arr) { 547 const tokens = itemToTokens(item); 548 for (const token of tokens) { 549 let currentMap = map; 550 for (let i = 0; i < token.length; i++) { 551 const char = token.charAt(i); 552 let nextMap = currentMap.get(char); 553 if (!nextMap) { 554 nextMap = new Map(); 555 currentMap.set(char, nextMap); 556 } 557 currentMap = nextMap; 558 } 559 let valuesAtCoda = currentMap.get(CODA_MARKER); 560 if (!valuesAtCoda) { 561 valuesAtCoda = []; 562 currentMap.set(CODA_MARKER, valuesAtCoda); 563 } 564 valuesAtCoda.push(item); 565 } 566 } 567 568 const search = (query, exact) => { 569 let currentMap = map; 570 for (let i = 0; i < query.length; i++) { 571 const char = query.charAt(i); 572 const nextMap = currentMap.get(char); 573 if (nextMap) { 574 currentMap = nextMap; 575 } else { 576 return [] 577 } 578 } 579 580 if (exact) { 581 const results = currentMap.get(CODA_MARKER); 582 return results || [] 583 } 584 585 const results = []; 586 // traverse 587 const queue = [currentMap]; 588 while (queue.length) { 589 const currentMap = queue.shift(); 590 const entriesSortedByKey = [...currentMap.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1); 591 for (const [key, value] of entriesSortedByKey) { 592 if (key === CODA_MARKER) { // CODA_MARKER always comes first; it's the empty string 593 results.push(...value); 594 } else { 595 queue.push(value); 596 } 597 } 598 } 599 return results 600 }; 601 602 return search 603} 604 605const requiredKeys$1 = [ 606 'name', 607 'url' 608]; 609 610function assertCustomEmojis (customEmojis) { 611 const isArray = customEmojis && Array.isArray(customEmojis); 612 const firstItemIsFaulty = isArray && 613 customEmojis.length && 614 (!customEmojis[0] || requiredKeys$1.some(key => !(key in customEmojis[0]))); 615 if (!isArray || firstItemIsFaulty) { 616 throw new Error('Custom emojis are in the wrong format') 617 } 618} 619 620function customEmojiIndex (customEmojis) { 621 assertCustomEmojis(customEmojis); 622 623 const sortByName = (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; 624 625 // 626 // all() 627 // 628 const all = customEmojis.sort(sortByName); 629 630 // 631 // search() 632 // 633 const emojiToTokens = emoji =>
vendor: 5,785 bytes, lines 633-831
633( 634 [...new Set((emoji.shortcodes || []).map(shortcode => extractTokens(shortcode)).flat())] 635 ); 636 const searchTrie = trie(customEmojis, emojiToTokens); 637 const searchByExactMatch = _ => searchTrie(_, true); 638 const searchByPrefix = _ => searchTrie(_, false); 639 640 // Search by query for custom emoji. Similar to how we do this in IDB, the last token 641 // is treated as a prefix search, but every other one is treated as an exact match. 642 // Then we AND the results together 643 const search = query => { 644 const tokens = extractTokens(query); 645 const intermediateResults = tokens.map((token, i) => ( 646 (i < tokens.length - 1 ? searchByExactMatch : searchByPrefix)(token) 647 )); 648 return findCommonMembers(intermediateResults, _ => _.name).sort(sortByName) 649 }; 650 651 // 652 // byShortcode, byName 653 // 654 const shortcodeToEmoji = new Map(); 655 const nameToEmoji = new Map(); 656 for (const customEmoji of customEmojis) { 657 nameToEmoji.set(customEmoji.name.toLowerCase(), customEmoji); 658 for (const shortcode of (customEmoji.shortcodes || [])) { 659 shortcodeToEmoji.set(shortcode.toLowerCase(), customEmoji); 660 } 661 } 662 663 const byShortcode = shortcode => shortcodeToEmoji.get(shortcode.toLowerCase()); 664 const byName = name => nameToEmoji.get(name.toLowerCase()); 665 666 return { 667 all, 668 search, 669 byShortcode, 670 byName 671 } 672} 673 674// remove some internal implementation details, i.e. the "tokens" array on the emoji object 675// essentially, convert the emoji from the version stored in IDB to the version used in-memory 676function cleanEmoji (emoji) { 677 if (!emoji) { 678 return emoji 679 } 680 delete emoji.tokens; 681 if (emoji.skinTones) { 682 const len = emoji.skinTones.length; 683 emoji.skins = Array(len); 684 for (let i = 0; i < len; i++) { 685 emoji.skins[i] = { 686 tone: emoji.skinTones[i], 687 unicode: emoji.skinUnicodes[i], 688 version: emoji.skinVersions[i] 689 }; 690 } 691 delete emoji.skinTones; 692 delete emoji.skinUnicodes; 693 delete emoji.skinVersions; 694 } 695 return emoji 696} 697 698function warnETag (eTag) { 699 if (!eTag) { 700 console.warn('emoji-picker-element is more efficient if the dataSource server exposes an ETag header.'); 701 } 702} 703 704const requiredKeys = [ 705 'annotation', 706 'emoji', 707 'group', 708 'order', 709 'tags', 710 'version' 711]; 712 713function assertEmojiData (emojiData) { 714 if (!emojiData || 715 !Array.isArray(emojiData) || 716 !emojiData[0] || 717 (typeof emojiData[0] !== 'object') || 718 requiredKeys.some(key => (!(key in emojiData[0])))) { 719 throw new Error('Emoji data is in the wrong format') 720 } 721} 722 723function assertStatus (response, dataSource) { 724 if (Math.floor(response.status / 100) !== 2) { 725 throw new Error('Failed to fetch: ' + dataSource + ': ' + response.status) 726 } 727} 728 729async function getETag (dataSource) { 730 const response = await fetch(dataSource, { method: 'HEAD' }); 731 assertStatus(response, dataSource); 732 const eTag = response.headers.get('etag'); 733 warnETag(eTag); 734 return eTag 735} 736 737async function getETagAndData (dataSource) { 738 const response = await fetch(dataSource); 739 assertStatus(response, dataSource); 740 const eTag = response.headers.get('etag'); 741 warnETag(eTag); 742 const emojiData = await response.json(); 743 assertEmojiData(emojiData); 744 return [eTag, emojiData] 745} 746 747// TODO: including these in blob-util.ts causes typedoc to generate docs for them, 748/** 749 * Convert an `ArrayBuffer` to a binary string. 750 * 751 * Example: 752 * 753 * ```js 754 * var myString = blobUtil.arrayBufferToBinaryString(arrayBuff) 755 * ``` 756 * 757 * @param buffer - array buffer 758 * @returns binary string 759 */ 760function arrayBufferToBinaryString(buffer) { 761 var binary = ''; 762 var bytes = new Uint8Array(buffer); 763 var length = bytes.byteLength; 764 var i = -1; 765 while (++i < length) { 766 binary += String.fromCharCode(bytes[i]); 767 } 768 return binary; 769} 770/** 771 * Convert a binary string to an `ArrayBuffer`. 772 * 773 * ```js 774 * var myBuffer = blobUtil.binaryStringToArrayBuffer(binaryString) 775 * ``` 776 * 777 * @param binary - binary string 778 * @returns array buffer 779 */ 780function binaryStringToArrayBuffer(binary) { 781 var length = binary.length; 782 var buf = new ArrayBuffer(length); 783 var arr = new Uint8Array(buf); 784 var i = -1; 785 while (++i < length) { 786 arr[i] = binary.charCodeAt(i); 787 } 788 return buf; 789} 790 791// generate a checksum based on the stringified JSON 792async function jsonChecksum (object) { 793 const inString = JSON.stringify(object); 794 const inBuffer = binaryStringToArrayBuffer(inString); 795 // this does not need to be cryptographically secure, SHA-1 is fine 796 const outBuffer = await crypto.subtle.digest('SHA-1', inBuffer); 797 const outBinString = arrayBufferToBinaryString(outBuffer); 798 const res = btoa(outBinString); 799 return res 800} 801 802async function checkForUpdates (db, dataSource) { 803 // just do a simple HEAD request first to see if the eTags match 804 let emojiData; 805 let eTag = await getETag(dataSource); 806 if (!eTag) { // work around lack of ETag/Access-Control-Expose-Headers 807 const eTagAndData = await getETagAndData(dataSource); 808 eTag = eTagAndData[0]; 809 emojiData = eTagAndData[1]; 810 if (!eTag) { 811 eTag = await jsonChecksum(emojiData); 812 } 813 } 814 if (await hasData(db, dataSource, eTag)) ; else { 815 if (!emojiData) { 816 const eTagAndData = await getETagAndData(dataSource); 817 emojiData = eTagAndData[1]; 818 } 819 await loadData(db, emojiData, dataSource, eTag); 820 } 821} 822 823async function loadDataForFirstTime (db, dataSource) { 824 let [eTag, emojiData] = await getETagAndData(dataSource); 825 if (!eTag) { 826 // Handle lack of support for ETag or Access-Control-Expose-Headers 827 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility 828 eTag = await jsonChecksum(emojiData); 829 } 830 831 await loadData(db, emojiData, dataSource, eTag);
vendor: 4,154 bytes, lines 831-972
831 832} 833 834class Database { 835 constructor ({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) { 836 this.dataSource = dataSource; 837 this.locale = locale; 838 this._dbName = `emoji-picker-element-${this.locale}`; 839 this._db = undefined; 840 this._lazyUpdate = undefined; 841 this._custom = customEmojiIndex(customEmoji); 842 843 this._clear = this._clear.bind(this); 844 this._ready = this._init(); 845 } 846 847 async _init () { 848 const db = this._db = await openDatabase(this._dbName); 849 850 addOnCloseListener(this._dbName, this._clear); 851 const dataSource = this.dataSource; 852 const empty = await isEmpty(db); 853 854 if (empty) { 855 await loadDataForFirstTime(db, dataSource); 856 } else { // offline-first - do an update asynchronously 857 this._lazyUpdate = checkForUpdates(db, dataSource); 858 } 859 } 860 861 async ready () { 862 const checkReady = async () => { 863 if (!this._ready) { 864 this._ready = this._init(); 865 } 866 return this._ready 867 }; 868 await checkReady(); 869 // There's a possibility of a race condition where the element gets added, removed, and then added again 870 // with a particular timing, which would set the _db to undefined. 871 // We *could* do a while loop here, but that seems excessive and could lead to an infinite loop. 872 if (!this._db) { 873 await checkReady(); 874 } 875 } 876 877 async getEmojiByGroup (group) { 878 assertNumber(group); 879 await this.ready(); 880 return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji) 881 } 882 883 async getEmojiBySearchQuery (query) { 884 assertNonEmptyString(query); 885 await this.ready(); 886 const customs = this._custom.search(query); 887 const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji); 888 return [ 889 ...customs, 890 ...natives 891 ] 892 } 893 894 async getEmojiByShortcode (shortcode) { 895 assertNonEmptyString(shortcode); 896 await this.ready(); 897 const custom = this._custom.byShortcode(shortcode); 898 if (custom) { 899 return custom 900 } 901 return cleanEmoji(await getEmojiByShortcode(this._db, shortcode)) 902 } 903 904 async getEmojiByUnicodeOrName (unicodeOrName) { 905 assertNonEmptyString(unicodeOrName); 906 await this.ready(); 907 const custom = this._custom.byName(unicodeOrName); 908 if (custom) { 909 return custom 910 } 911 return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName)) 912 } 913 914 async getPreferredSkinTone () { 915 await this.ready(); 916 return (await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE)) || 0 917 } 918 919 async setPreferredSkinTone (skinTone) { 920 assertNumber(skinTone); 921 await this.ready(); 922 return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone) 923 } 924 925 async incrementFavoriteEmojiCount (unicodeOrName) { 926 assertNonEmptyString(unicodeOrName); 927 await this.ready(); 928 return incrementFavoriteEmojiCount(this._db, unicodeOrName) 929 } 930 931 async getTopFavoriteEmoji (limit) { 932 assertNumber(limit); 933 await this.ready(); 934 return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji) 935 } 936 937 set customEmoji (customEmojis) { 938 this._custom = customEmojiIndex(customEmojis); 939 } 940 941 get customEmoji () { 942 return this._custom.all 943 } 944 945 async _shutdown () { 946 await this.ready(); // reopen if we've already been closed/deleted 947 try { 948 await this._lazyUpdate; // allow any lazy updates to process before closing/deleting 949 } catch (err) { /* ignore network errors (offline-first) */ } 950 } 951 952 // clear references to IDB, e.g. during a close event 953 _clear () { 954 // We don't need to call removeEventListener or remove the manual "close" listeners. 955 // The memory leak tests prove this is unnecessary. It's because: 956 // 1) IDBDatabases that can no longer fire "close" automatically have listeners GCed 957 // 2) we clear the manual close listeners in databaseLifecycle.js. 958 this._db = this._ready = this._lazyUpdate = undefined; 959 } 960 961 async close () { 962 await this._shutdown(); 963 await closeDatabase(this._dbName); 964 } 965 966 async delete () { 967 await this._shutdown(); 968 await deleteDatabase(this._dbName); 969 } 970} 971 972export { Database as default };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.