<commit_before>/**
 *
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

import Constants from '../constants/constants';

class OfflineCache {

  static has (name) {
    if (!Constants.SUPPORTS_CACHING) {
      return Promise.resolve(false);
    }

    name = this.convertPathToName(name);
    return this.purgePartialDownloads(name).then(_ => {
      return caches.has(name);
    });
  }

  static purgePartialDownloads (name) {
    name = this.convertPathToName(name);
    return caches.has(name).then(hasCacheOfName => {
      if (!hasCacheOfName) {
        return;
      }

      return caches.open(name)
          // 1. Get all the items in the cache.
          .then(cache => cache.keys())
          .then(cacheItems => {
            // 2. Locate the chunks, if there are any.
            let maxIndex = Number.NEGATIVE_INFINITY;
            let url = '';

            cacheItems.forEach(item => {
              const endsInNumber = /_(\d+)$/.exec(item.url);
              if (!endsInNumber) {
                return;
              }

              const chunkIndex = parseInt(endsInNumber[1], 10);
              if (chunkIndex < maxIndex) {
                return;
              }

              maxIndex = chunkIndex;
              url = item.url;
            });

            // No chunks stored at all - delete.
            if (maxIndex === Number.NEGATIVE_INFINITY || url === '') {
              return caches.delete(name);
            }

            // 3. Compare the largest chunk index to what we'd expect to have.
            return caches.match(url).then(chunk => {
              return parseInt(chunk.headers.get('Content-Length'), 10);
            }).then(videoSize => {
              if (isNaN(videoSize)) {
                return;
              }

              const expectedIndex =
                  Math.floor(videoSize / Constants.CHUNK_SIZE);
              if (expectedIndex == maxIndex) {
                return;
              }

              // This is a partial download - purge it.
              console.log(`Purging ${name}`);
              return caches.delete(name);
            });
          });
    });
  }

  static hasPrefetched (assetPath) {
    if (!Constants.SUPPORTS_CACHING) {
      return Promise.resolve(false);
    }

    return caches.open('prefetch').then(cache => {
      return cache.match(`${assetPath}/${Constants.PREFETCH_VIDEO_PATH}_0`);
    });
  }

  static removeAllPrefetched () {
    return caches.delete('prefetch');
  }

  static getAll () {
    return caches.keys().then(caches => {
      return caches.filter(c => {
        return !(c === 'prefetch' || c.startsWith(Constants.APP_NAME));
      });
    });
  }

  static convertPathToName (path) {
    return path
        .replace(/^\//, '')
        .replace(/\/$/, '');
  }

  constructor () {
    this._cancel = new Set();
  }

  cancel (name) {
    name = OfflineCache.convertPathToName(name);
    return OfflineCache.has(name).then(hasCache => {
      // Can't cancel if the item is already cached.
      if (hasCache) {
        return Promise.reject();
      }

      console.log('Setting cancel for ' + name);
      this._cancel.add(name);
    });
  }

  add (name, assetPath, pagePath, callbacks) {
    if (!Constants.SUPPORTS_CACHING) {
      return;
    }

    const assets = [];
    const assetsToCache = [
      // The meta asssets.
      ...Constants.OFFLINE_ASSET_LIST,

      // The video.
      {
        src: Constants.OFFLINE_VIDEO_PATH,
        chunk: true
      },

      // The audio.
      {
        src: Constants.OFFLINE_AUDIO_PATH,
        chunk: true
      }
    ];

    // The meta assets.
    assetsToCache.forEach(asset => {
      const src = asset.src || asset;
      const dest = asset.dest || asset.src || asset;
      const chunk = asset.chunk || false;

      assets.push({
        request: `${assetPath}/${dest}`,
        response: fetch(`${assetPath}/${src}`, {mode: 'cors'}),
        chunk
      });
    });

    // Ensure that the request for the page isn't gzipped in response.
    const headers = new Headers();
    headers.set('X-No-Compression', true);

    assets.push({
      request: pagePath,
      response: fetch(pagePath, {
        headers
      })
    });

    return this._download(name, assets, callbacks);
  }

  prefetch (manifestPath, prefetchLimit=30) {
    const makeRequest = ({path, start, end, chunk}={}) => {
      const headers = new Headers();
      if (typeof start !== 'undefined' && typeof end !== 'undefined') {
        headers.set('range', `bytes=${start}-${end}`);

        if (!chunk) {
          throw new Error(`Unable to cache unchunked, ranged requests.
              (${path})`);
        }
      }

      const response = fetch(path, {
        mode: 'cors',
        headers
      });

      return {
        request: path,
        response,
        chunk
      };
    };

    return this._getManifest(manifestPath)
        .then(manifest => this._getRanges(manifestPath, manifest))
        .then(ranges => {
          return Promise.all([<commit_msg>Ensures Shaka is there before enabling precache.<commit_after>/**
 *
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

import Constants from '../constants/constants';

class OfflineCache {

  static has (name) {
    if (!Constants.SUPPORTS_CACHING) {
      return Promise.resolve(false);
    }

    name = this.convertPathToName(name);
    return this.purgePartialDownloads(name).then(_ => {
      return caches.has(name);
    });
  }

  static purgePartialDownloads (name) {
    name = this.convertPathToName(name);
    return caches.has(name).then(hasCacheOfName => {
      if (!hasCacheOfName) {
        return;
      }

      return caches.open(name)
          // 1. Get all the items in the cache.
          .then(cache => cache.keys())
          .then(cacheItems => {
            // 2. Locate the chunks, if there are any.
            let maxIndex = Number.NEGATIVE_INFINITY;
            let url = '';

            cacheItems.forEach(item => {
              const endsInNumber = /_(\d+)$/.exec(item.url);
              if (!endsInNumber) {
                return;
              }

              const chunkIndex = parseInt(endsInNumber[1], 10);
              if (chunkIndex < maxIndex) {
                return;
              }

              maxIndex = chunkIndex;
              url = item.url;
            });

            // No chunks stored at all - delete.
            if (maxIndex === Number.NEGATIVE_INFINITY || url === '') {
              return caches.delete(name);
            }

            // 3. Compare the largest chunk index to what we'd expect to have.
            return caches.match(url).then(chunk => {
              return parseInt(chunk.headers.get('Content-Length'), 10);
            }).then(videoSize => {
              if (isNaN(videoSize)) {
                return;
              }

              const expectedIndex =
                  Math.floor(videoSize / Constants.CHUNK_SIZE);
              if (expectedIndex == maxIndex) {
                return;
              }

              // This is a partial download - purge it.
              console.log(`Purging ${name}`);
              return caches.delete(name);
            });
          });
    });
  }

  static hasPrefetched (assetPath) {
    if (!Constants.SUPPORTS_CACHING) {
      return Promise.resolve(false);
    }

    return caches.open('prefetch').then(cache => {
      return cache.match(`${assetPath}/${Constants.PREFETCH_VIDEO_PATH}_0`);
    });
  }

  static removeAllPrefetched () {
    return caches.delete('prefetch');
  }

  static getAll () {
    return caches.keys().then(caches => {
      return caches.filter(c => {
        return !(c === 'prefetch' || c.startsWith(Constants.APP_NAME));
      });
    });
  }

  static convertPathToName (path) {
    return path
        .replace(/^\//, '')
        .replace(/\/$/, '');
  }

  constructor () {
    this._cancel = new Set();
  }

  cancel (name) {
    name = OfflineCache.convertPathToName(name);
    return OfflineCache.has(name).then(hasCache => {
      // Can't cancel if the item is already cached.
      if (hasCache) {
        return Promise.reject();
      }

      console.log('Setting cancel for ' + name);
      this._cancel.add(name);
    });
  }

  add (name, assetPath, pagePath, callbacks) {
    if (!Constants.SUPPORTS_CACHING) {
      return;
    }

    const assets = [];
    const assetsToCache = [
      // The meta asssets.
      ...Constants.OFFLINE_ASSET_LIST,

      // The video.
      {
        src: Constants.OFFLINE_VIDEO_PATH,
        chunk: true
      },

      // The audio.
      {
        src: Constants.OFFLINE_AUDIO_PATH,
        chunk: true
      }
    ];

    // The meta assets.
    assetsToCache.forEach(asset => {
      const src = asset.src || asset;
      const dest = asset.dest || asset.src || asset;
      const chunk = asset.chunk || false;

      assets.push({
        request: `${assetPath}/${dest}`,
        response: fetch(`${assetPath}/${src}`, {mode: 'cors'}),
        chunk
      });
    });

    // Ensure that the request for the page isn't gzipped in response.
    const headers = new Headers();
    headers.set('X-No-Compression', true);

    assets.push({
      request: pagePath,
      response: fetch(pagePath, {
        headers
      })
    });

    return this._download(name, assets, callbacks);
  }

  prefetch (manifestPath, prefetchLimit=30) {
    if (typeof shaka === 'undefined') {
      return;
    }

    const makeRequest = ({path, start, end, chunk}={}) => {
      const headers = new Headers();
      if (typeof start !== 'undefined' && typeof end !== 'undefined') {
        headers.set('range', `bytes=${start}-${end}`);

        if (!chunk) {
          throw new Error(`Unable to cache unchunked, ranged requests.
              (${path})`);
        }
      }

      const response = fetch(path, {
        mode: 'cors',
        headers
      });

      return {
        request: path,
        response,
        chunk
      };
    };

    return this._getManifest(manifestPath)
        .then(manifest => this._getRanges(manifestPath, manifest))
        .then(ranges => {
          return Promise.all([
{'fraction_non_alphanumeric': 0.12921745274686453, 'fraction_numerical': 0.0033562974739445328, 'mean_word_length': 1.9100488306347982, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4142405', 'n_tokens_mistral': 3301, 'n_tokens_neox': 3209, 'n_words': 820}