PageSourceSearch

https://w.app/emoji-picker/database.js

js w.app collected 2026-10-01 12:22:27 UTC 28,951 bytes, 972 lines download raw bytes

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.