├── extension
├── .env
├── .gitignore
├── README.md
├── .DS_Store
├── src
│ ├── .DS_Store
│ ├── pages
│ │ ├── .DS_Store
│ │ ├── Panel
│ │ │ ├── .DS_Store
│ │ │ ├── dist.zip
│ │ │ ├── images
│ │ │ │ ├── 128.png
│ │ │ │ ├── 16.png
│ │ │ │ ├── 48.png
│ │ │ │ └── lightning.png
│ │ │ ├── dist
│ │ │ │ ├── ab5c74f3bce639503765.js.LICENSE.txt
│ │ │ │ ├── devtools.bundle.js
│ │ │ │ ├── 2eaaee49a77af63e6b86.js
│ │ │ │ ├── panel.html
│ │ │ │ ├── devtools.html
│ │ │ │ ├── manifest.json
│ │ │ │ └── panel.bundle.js.LICENSE.txt
│ │ │ ├── index.html
│ │ │ ├── App.jsx
│ │ │ ├── index.js
│ │ │ ├── styles.css
│ │ │ └── Components
│ │ │ │ ├── Requests.jsx
│ │ │ │ └── Metrics.jsx
│ │ └── DevTools
│ │ │ ├── index.html
│ │ │ └── index.js
│ ├── manifest.json
│ └── background.js
├── .babelrc
├── package.json
└── webpack.config.js
├── demo
├── README.md
├── client
│ ├── stylesheets
│ │ ├── background.png
│ │ └── application.css
│ ├── components
│ │ ├── Query.jsx
│ │ ├── BookDisplay.jsx
│ │ └── App.jsx
│ └── index.js
├── index.html
└── server
│ ├── model
│ └── bookModel.js
│ ├── routes
│ └── booksRouter.js
│ ├── server.js
│ └── controller
│ └── booksController.js
├── .gitignore
├── .babelrc
├── .npmignore
├── .DS_Store
├── jest.config.js
├── __tests__
├── serverRequest.test.js
├── flacheRequest.test.js
├── validateCache.test.js
├── generateKey.test.js
├── synthResponse.test.js
└── flache.test.js
├── index.html
├── webpack.dev.js
├── src
├── helpers
│ ├── reqExtension.js
│ ├── validateCache.js
│ ├── serverRequest.js
│ ├── generateKey.js
│ ├── copyResponse.js
│ ├── parsers.js
│ ├── synthResponse.js
│ └── flacheRequest.js
├── flache.js
└── flacheStorage.js
├── jest.setup.js
├── webpack.prod.js
├── webpack.demo.js
├── package.json
├── README.md
└── dist
├── flache.min.js
├── flache.js.map
├── flache.js
└── flache.min.js.map
/extension/.env:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # OSP
2 |
--------------------------------------------------------------------------------
/extension/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
--------------------------------------------------------------------------------
/extension/README.md:
--------------------------------------------------------------------------------
1 | # Flache DevTool
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"]
3 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | demo
3 | __tests__
4 | extension
5 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/.DS_Store
--------------------------------------------------------------------------------
/extension/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/.DS_Store
--------------------------------------------------------------------------------
/extension/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/.DS_Store
--------------------------------------------------------------------------------
/extension/src/pages/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/.DS_Store
--------------------------------------------------------------------------------
/extension/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ]
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/extension/src/pages/Panel/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/Panel/.DS_Store
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/Panel/dist.zip
--------------------------------------------------------------------------------
/demo/client/stylesheets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/demo/client/stylesheets/background.png
--------------------------------------------------------------------------------
/extension/src/pages/Panel/images/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/Panel/images/128.png
--------------------------------------------------------------------------------
/extension/src/pages/Panel/images/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/Panel/images/16.png
--------------------------------------------------------------------------------
/extension/src/pages/Panel/images/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/Panel/images/48.png
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/ab5c74f3bce639503765.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! For license information please see panel.bundle.js.LICENSE.txt */
2 |
--------------------------------------------------------------------------------
/extension/src/pages/Panel/images/lightning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/flachejs/HEAD/extension/src/pages/Panel/images/lightning.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "verbose": true,
3 | "testEnvironment": "jest-environment-jsdom",
4 | "automock": false,
5 | "collectCoverage": true,
6 | "setupFiles": ['./jest.setup.js']
7 | }
8 |
--------------------------------------------------------------------------------
/__tests__/serverRequest.test.js:
--------------------------------------------------------------------------------
1 | import getFetchRequest from '../src/helpers/serverRequest';
2 |
3 | describe('Testing for getFetchRequest function', () => {
4 |
5 | test('temporary', () => {
6 | expect(1).toBe(1);
7 | })
8 |
9 | })
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/devtools.bundle.js:
--------------------------------------------------------------------------------
1 | chrome.devtools.panels.create("Flache",null,"/pages/Panel/index.html"),chrome.runtime.onMessageExternal.addListener((function(e,o,n){console.log("This is the response from devtools: ",e)}));
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/2eaaee49a77af63e6b86.js:
--------------------------------------------------------------------------------
1 | chrome.devtools.panels.create("Flache",null,"/pages/Panel/index.html"),chrome.runtime.onMessageExternal.addListener((function(e,o,n){console.log("This is the response from devtools: ",e)}));
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/panel.html:
--------------------------------------------------------------------------------
1 |
Flache
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | flache demo
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/extension/src/pages/Panel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Flache
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/extension/src/pages/Panel/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Metrics from './Components/Metrics';
3 | import Requests from './Components/Requests';
4 |
5 | const App = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default App;
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/devtools.html:
--------------------------------------------------------------------------------
1 | Flache
--------------------------------------------------------------------------------
/extension/src/pages/Panel/index.js:
--------------------------------------------------------------------------------
1 | // This is a temporary file to test out code in panel.html
2 | // installed d3 to test it out;
3 |
4 | import React from 'react';
5 | import ReactDOM from 'react-dom/client';
6 | import App from './App';
7 | import './styles.css';
8 |
9 | const root = ReactDOM.createRoot(document.getElementById('root'));
10 | root.render(
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/extension/src/pages/DevTools/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Flache
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | flache
8 |
9 |
10 | FLACHE DEMO
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/manifest.json:
--------------------------------------------------------------------------------
1 | {"version":"1.0","name":"Flache","manifest_version":3,"minimum_chrome_version":"88","devtools_page":"./pages/DevTools/index.html","icons":{"16":"./pages/Panel/images/16.png","48":"./pages/Panel/images/48.png","128":"./pages/Panel/images/128.png"},"externally_connectable":{"matches":["*://localhost/*"]},"background":{"service_worker":"background.js"},"permissions":["storage","idle","*://localhost/*"]}
--------------------------------------------------------------------------------
/demo/client/components/Query.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loki from 'lokijs';
3 |
4 | export const db = new Loki('client-cache', {
5 | autoload: true,
6 | autosave: true,
7 | autosaveInterval: 4000
8 | });
9 |
10 | export const requests = db.addCollection('requests');
11 |
12 | const queryContext = React.createContext(requests);;
13 |
14 | export const QueryProvider = queryContext.Provider;
15 |
16 | export default queryContext;
17 |
18 |
--------------------------------------------------------------------------------
/extension/src/pages/DevTools/index.js:
--------------------------------------------------------------------------------
1 | chrome.devtools.panels.create('Flache', null, '/pages/Panel/index.html');
2 |
3 | chrome.runtime.onMessageExternal.addListener(
4 | function(request, sender, sendResponse) {
5 | console.log('This is the response from devtools: ',request);
6 |
7 | /* if (sender.url === blocklistedWebsite)
8 | return; // don't allow this web page access
9 | if (request.openUrlInEditor)
10 | openUrl(request.openUrlInEditor); */
11 | });
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: {
7 | index: './src/flache.js'
8 | },
9 |
10 | output: {
11 | filename: 'bundle.js',
12 | path: path.resolve(__dirname, 'build'),
13 | },
14 |
15 | plugins: [
16 | new HtmlWebpackPlugin({
17 | template:'index.html'
18 | })
19 | ],
20 | devServer: {
21 | compress: true,
22 | port: 8080,
23 | }
24 | }
--------------------------------------------------------------------------------
/demo/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import * as ReactDOMClient from 'react-dom/client';
3 | import App from './components/App.jsx';
4 |
5 | import styles from './stylesheets/application.css';
6 |
7 | const container = document.getElementById('app');
8 |
9 | // Create a root.
10 | const root = ReactDOMClient.createRoot(container);
11 |
12 | // Initial render: Render an element to the root.
13 | root.render(
14 | //
15 |
16 | //
17 | );
18 |
19 |
--------------------------------------------------------------------------------
/demo/server/model/bookModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const bookSchema = new Schema({
5 | title: { type: String, required: true },
6 | subtitle: { type: String },
7 | category: {type: String, required: true},
8 | isbn13: { type: String, required: true },
9 | price: { type: String, required: true },
10 | image: { type: String, required: true },
11 | url: { type: String, required: true }
12 | })
13 |
14 | module.exports = mongoose.model('books', bookSchema);
--------------------------------------------------------------------------------
/src/helpers/reqExtension.js:
--------------------------------------------------------------------------------
1 | const reqExtension = (url, duration, inCache, TTL) => {
2 | // Send data to our Extension
3 | if(chrome && chrome.runtime && chrome.runtime.sendMessage) {
4 | async function sendReq () {
5 | let aRequest = {
6 | requestURL: url,
7 | time: duration,
8 | inCache: inCache,
9 | ttl: TTL
10 | }
11 | chrome.runtime.sendMessage("bmkhjogdgeafjdanmhjddmcldejpgaga", aRequest);
12 | }
13 | sendReq();
14 | }
15 | }
16 |
17 | export default reqExtension;
--------------------------------------------------------------------------------
/extension/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Flache",
3 | "version": "1.0",
4 | "manifest_version": 3,
5 | "minimum_chrome_version": "88",
6 | "devtools_page": "./pages/DevTools/index.html",
7 | "icons": {
8 | "16": "./pages/Panel/images/16.png",
9 | "48": "./pages/Panel/images/48.png",
10 | "128": "./pages/Panel/images/128.png"
11 | },
12 | "externally_connectable": {
13 | "matches": [
14 | "*://localhost/*"
15 | ]
16 | },
17 | "background": {
18 | "service_worker": "background.js"
19 | },
20 | "permissions" : [
21 | "storage",
22 | "idle",
23 | "*://localhost/*"
24 | ]
25 | }
--------------------------------------------------------------------------------
/src/helpers/validateCache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Function that validates the cache
3 | * @param {string} uniqueKey - hashed key that contains request url and body
4 | * @param {object} data - response from the server request containing the request's TTL
5 | * @return {object} object cache value (keys: ttl, data) if in cache and valid, null if not
6 | */
7 | async function validateCache(uniqueKey, data) {
8 | // check if the item in the store's TTL has passed from the current time of the function call
9 | if (data.ttl < Date.now()) {
10 | await this.store.removeItem(uniqueKey);
11 | return null;
12 | } else return data;
13 | }
14 |
15 | export default validateCache;
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | /**
3 | * This allows us to mock the Response object with the actually object from Node
4 | * Also added the headers Object for testing.
5 | */
6 | const realRes = require('node-fetch');
7 | global.Response = realRes.Response;
8 | global.Headers = realRes.Headers;
9 |
10 | /**
11 | * This will allow us to ignore console logs from outside our test our code
12 | * Note: must pass true as the last argument to have the log show up.
13 | */
14 |
15 | const consoleLog = global.console.log;
16 | global.console.log = (...args) => {
17 | const test = args[args.length - 1];
18 | if (test !== true) return
19 | consoleLog(...args)
20 | }
21 | }
--------------------------------------------------------------------------------
/demo/client/components/BookDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | const BookDisplay = props => {
4 | const {data} = props;
5 |
6 | return (
7 |
8 |
9 |

10 |
11 |
12 | - {data.title}
13 | - {data.subtitle}
14 | - Category: {data.category}
15 | - Price: {data.price}
16 |
17 |
18 | );
19 | };
20 |
21 | export default BookDisplay;
--------------------------------------------------------------------------------
/__tests__/flacheRequest.test.js:
--------------------------------------------------------------------------------
1 | import flacheRequest from '../src/helpers/flacheRequest';
2 | import clientCache from '../src/flache';
3 | import fetchMock from 'jest-fetch-mock';
4 | // import localforage from 'localforage';
5 | // jest.mock('localforage')
6 |
7 | describe('Testing for getFetchRequest function', () => {
8 |
9 | test('temporary', () => {
10 | expect(1).toBe(1);
11 | })
12 |
13 | // const url = 'https://swapi.dev/api/people';
14 | // let cache;
15 |
16 | // beforeAll(() => {
17 | // cache = new clientCache();
18 | // fetchMock.enableMocks();
19 | // });
20 |
21 | // beforeEach(() => {
22 | // fetchMock.resetMocks();
23 | // cache.store.clear();
24 | // cache.flacheRequest(url);
25 | // });
26 |
27 | })
--------------------------------------------------------------------------------
/demo/server/routes/booksRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const booksController = require('../controller/booksController');
4 |
5 | // View all books
6 | router.get('/', booksController.view, (req, res) => {
7 | res.json(res.locals.allEntries);
8 | });
9 |
10 | // Get Books by Category
11 | router.get('/:category',
12 | booksController.getBooksByCategory,
13 | (req, res) => res.json(res.locals.books)
14 | );
15 |
16 | // Add fetched books to database (to populate db)
17 | // USE POSTMAN: POST - http://localhost:3000/bookshelf/populatedb
18 | router.post('/populatedb',
19 | booksController.fetchBooks,
20 | booksController.addFetchedBooks,
21 | (req, res) => res.json({})
22 | );
23 |
24 | module.exports = router;
--------------------------------------------------------------------------------
/src/helpers/serverRequest.js:
--------------------------------------------------------------------------------
1 | import parseResponse from './parsers';
2 |
3 | /**
4 | * Function that makes a Fetch request to the server
5 | * @param {string} url URL to where fetch request is being made
6 | * @return {object} Object containing the resulting data from executing the request in the server
7 | */
8 |
9 | /**
10 | * How will we handle being redirected?
11 | */
12 |
13 | async function getFetchRequest(url, options) {
14 | // TO-DO handling headers, response-types, etc for how to parse data;
15 | let response = await fetch(url, options)
16 | .then(res => {
17 | const proxyResponse = parseResponse(res);
18 | return proxyResponse;
19 | })
20 | .catch(err => {
21 | console.log('Fetch error', err.message);
22 | return err;
23 | });
24 | return response;
25 | }
26 |
27 | export default getFetchRequest;
--------------------------------------------------------------------------------
/extension/src/pages/Panel/dist/panel.bundle.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /**
2 | * @license React
3 | * react-dom.production.min.js
4 | *
5 | * Copyright (c) Facebook, Inc. and its affiliates.
6 | *
7 | * This source code is licensed under the MIT license found in the
8 | * LICENSE file in the root directory of this source tree.
9 | */
10 |
11 | /**
12 | * @license React
13 | * react.production.min.js
14 | *
15 | * Copyright (c) Facebook, Inc. and its affiliates.
16 | *
17 | * This source code is licensed under the MIT license found in the
18 | * LICENSE file in the root directory of this source tree.
19 | */
20 |
21 | /**
22 | * @license React
23 | * scheduler.production.min.js
24 | *
25 | * Copyright (c) Facebook, Inc. and its affiliates.
26 | *
27 | * This source code is licensed under the MIT license found in the
28 | * LICENSE file in the root directory of this source tree.
29 | */
30 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | devtool: 'source-map',
7 | // optimization: {
8 | // minimize: false
9 | // },
10 |
11 | entry: {
12 | index: './src/flache.js'
13 | },
14 |
15 | output: {
16 | filename: 'flache.min.js',
17 | path: path.resolve(__dirname, 'dist'),
18 | // sourceMapFilename: 'flache.js.map',
19 | library: {
20 | name: 'flachejs',
21 | type: 'umd',
22 | }
23 | },
24 |
25 | externals: {
26 | localforage: {
27 | commonjs: 'localforage',
28 | commonjs2: 'localforage',
29 | amd: 'localforage',
30 | root: 'localforage',
31 | },
32 |
33 | md5: {
34 | commonjs: 'md5',
35 | commonjs2: 'md5',
36 | amd: 'md5',
37 | root: 'md5'
38 | }
39 | },
40 | }
--------------------------------------------------------------------------------
/src/helpers/generateKey.js:
--------------------------------------------------------------------------------
1 | import md5 from 'md5';
2 |
3 | /**
4 | * Function that takes in arguments of an HTTP request and returns them as a single unique (hashed) key
5 | * @param {string} url - request URL
6 | * @param {object} data - object containing request body such as the HTTP method
7 | * @return {string} - Hashed key
8 | **/
9 | // TO-DO consider including headers in our hashing strategy? If a POST request is made with different headers its conceivable that
10 | // the expected repsonse would be different;
11 | const generateKey = (url, data) => {
12 | // TO-DO error handling for incorrect method
13 | const method = data.method.toUpperCase();
14 | if (method === 'GET') {
15 | return (`${method}/${url}`);
16 | }
17 | if (method === 'POST') {
18 | if (!Object.hasOwn(data, 'body')) throw new Error('Must include a body with POST request');
19 | return (`${method}/${md5(JSON.stringify(data.body))}/${url}`);
20 | }
21 | }
22 |
23 | export default generateKey;
--------------------------------------------------------------------------------
/extension/src/pages/Panel/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: "Fantasy";
5 | justify-content: center;
6 | text-align: center;
7 | }
8 |
9 | h3, h4 {
10 | text-align: center;
11 | }
12 |
13 | body {
14 | background-color:#f6f2ef;
15 | display: flex;
16 | justify-content: center;
17 | font-family: monospace;
18 | font-size: 15px;
19 | }
20 |
21 | table, th, td {
22 | border: 1px solid black;
23 | border-collapse: collapse;
24 | font-family: monospace;
25 | font-size: 15px;
26 | }
27 |
28 | td {
29 | padding: 0 15px;
30 | }
31 |
32 | .TableFixed thead th {
33 | position: sticky; top: 0; z-index: 1;
34 | }
35 |
36 | .TableFixed {
37 | overflow: auto; height: 157px;
38 | font-size: 12px;
39 | }
40 |
41 | .info {
42 | padding-top: 10px;
43 | }
44 |
45 | .speed-graph {
46 | margin-bottom: 10px;
47 | }
48 |
49 | .noTTL {
50 | background-color: rgb(221,255,221);
51 | }
52 |
53 | .TTL {
54 | background-color: rgba(255, 0, 0, 0.2);
55 | }
--------------------------------------------------------------------------------
/src/helpers/copyResponse.js:
--------------------------------------------------------------------------------
1 | function copyResponse(res, skip = ['body']) {
2 | if (!(res instanceof Response)) throw new Error('Not a valid response object');
3 |
4 | const newObj = {};
5 | for (const key in res) {
6 | // this is to avoid copying function definitions from the objects prototype;
7 | // it also checks if we have marked this as a property ot skip.
8 | if (skip.includes(key) || typeof res[key] === 'function') continue;
9 |
10 | //This is to iterate through the headers obj and copy all of the headers returned by the server
11 | // we will reconstruct this later and recreate the exact same response.
12 | if (key === 'headers') {
13 | newObj[key] = copyHeaders(res[key]);
14 | continue;
15 | }
16 | newObj[key] = res[key];
17 | }
18 | return newObj;
19 | }
20 |
21 | function copyHeaders(header) {
22 | const entries = header.entries();
23 |
24 | const newObj = {};
25 |
26 | for (const [key, value] of entries) {
27 | newObj[key] = value;
28 | }
29 |
30 | return newObj
31 | }
32 |
33 | export default copyResponse
--------------------------------------------------------------------------------
/extension/src/background.js:
--------------------------------------------------------------------------------
1 |
2 | chrome.storage.sync.set({'request': {}}, function() {
3 | console.log('Key Request in Storage reset to empty.');
4 | });
5 |
6 | chrome.runtime.onMessageExternal.addListener(function(request, sender, sendResponse) {
7 | console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension");
8 | if (request.requestURL) {
9 | chrome.storage.sync.get('request', function(result) {
10 | if (result.request.url === undefined) {
11 | chrome.storage.sync.set({'request': {url: [request.requestURL], time: [request.time], inCache: [request.inCache], ttl: [request.ttl]}});
12 | }
13 | else {
14 | let urls = result.request.url;
15 | let reqs = result.request.time;
16 | let inCache = result.request.inCache;
17 | let ttls = result.request.ttl;
18 | urls.push(request.requestURL);
19 | reqs.push(request.time);
20 | inCache.push(request.inCache);
21 | ttls.push(request.ttl)
22 | chrome.storage.sync.set({'request': {url: urls, time: reqs, inCache: inCache, ttl: ttls}});
23 | }
24 | });
25 | }
26 | });
27 |
--------------------------------------------------------------------------------
/demo/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express();
3 | const cors = require("cors");
4 | const mongoose = require("mongoose");
5 | const PORT = 3000;
6 | require('dotenv').config();
7 |
8 | // Database Connection
9 | mongoose.connect(
10 | 'mongodb+srv://twojay:PtTS83QW2JVJsB7j@cluster0.ueoejsn.mongodb.net/?retryWrites=true&w=majority',
11 | { useNewUrlParser: true, useUnifiedTopology: true, dbName: "bookshelf" }
12 | )
13 | mongoose.connection.once("open", () => {
14 | console.log("Connected to Database");
15 | });
16 |
17 | // Express App Requirements
18 | app.use(express.urlencoded({ extended: true }));
19 | app.use(express.json());
20 | app.use(cors());
21 |
22 | const booksRouter = require("./routes/booksRouter.js");
23 | app.use("/bookshelf", booksRouter);
24 |
25 | // Global Error Handler
26 | app.use((err, req, res, next) => {
27 | const defaultErr = {
28 | log: 'Express Error Handler Caught Unknown Middleware Error',
29 | status: 400,
30 | message: { err: 'An error occurred' },
31 | };
32 | const errorObj = Object.assign({}, defaultErr, err);
33 | console.log(errorObj.log);
34 | return res.status(errorObj.status).json(errorObj.message);
35 | });
36 |
37 | // Start Server
38 | app.listen(PORT, () => {
39 | console.log(`Listening on Port: ${PORT}`);
40 | });
41 |
--------------------------------------------------------------------------------
/extension/src/pages/Panel/Components/Requests.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const Requests = () => {
4 | const [req, setReq] = useState(null);
5 |
6 | function getStorage () {
7 | chrome.storage.sync.get('request', function(result) {
8 | if (result != {}) {
9 | let allRequests = []
10 | for (let i = result.request.url.length - 1; i >= 0; i--) {
11 | let name = "TTL"
12 | if (result.request.ttl[i] === -1) name = "noTTL"
13 | allRequests.push(| {i+1} | {result.request.url[i]} |
14 | {result.request.time[i]} |
15 | {result.request.inCache[i]} |
)
16 | }
17 | setReq(allRequests)
18 | }
19 | });
20 | }
21 | chrome.storage.onChanged.addListener(getStorage);
22 | return (
23 |
24 |
Requests:
25 |
26 |
27 |
28 | | URL | Time (ms) | Cache |
29 |
30 |
31 | {req}
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default Requests;
--------------------------------------------------------------------------------
/extension/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flache",
3 | "version": "0.0.1",
4 | "description": "Example of using React for developing Google Chrome DevTools Extension",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server",
8 | "serve": "webpack serve --mode development",
9 | "build": "webpack --mode production",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "devDependencies": {
13 | "@babel/core": "^7.18.0",
14 | "@babel/preset-env": "^7.18.0",
15 | "@babel/preset-react": "^7.17.12",
16 | "babel-jest": "^28.1.1",
17 | "babel-loader": "^8.2.5",
18 | "concurrently": "^7.2.1",
19 | "cross-env": "^7.0.3",
20 | "css-loader": "^6.7.1",
21 | "file-loader": "^6.2.0",
22 | "html-webpack-plugin": "^5.5.0",
23 | "nodemon": "^2.0.16",
24 | "react": "^18.1.0",
25 | "react-dom": "^18.1.0",
26 | "request": "^2.88.2",
27 | "sass": "^1.53.0",
28 | "style-loader": "^3.3.1",
29 | "url-loader": "^4.1.1",
30 | "webpack": "^5.73.0",
31 | "webpack-cli": "^4.10.0",
32 | "webpack-dev-server": "^4.9.2"
33 | },
34 | "dependencies": {
35 | "copy-webpack-plugin": "^11.0.0",
36 | "d3": "^7.5.0",
37 | "html-loader": "^3.1.2",
38 | "react-trend": "^1.2.5",
39 | "sass-loader": "^13.0.2",
40 | "socket.io-client": "^4.5.1",
41 | "source-map-loader": "^4.0.0",
42 | "zustand": "^4.0.0-rc.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/webpack.demo.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | resolve: {
7 | fallback: {
8 | "fs": false,
9 | }
10 | },
11 | entry: {
12 | index: './demo/client/index.js'
13 | },
14 |
15 | output: {
16 | filename: 'bundle.js',
17 | path: path.resolve(__dirname, '/demo/build'),
18 | publicPath: '/build'
19 | },
20 |
21 | plugins: [
22 | new HtmlWebpackPlugin({
23 | template: path.resolve(__dirname, '/demo/index.html')
24 | }),
25 | ],
26 |
27 | module: {
28 | rules: [
29 | {
30 | test: /\.jsx?/,
31 | use: {
32 | loader: 'babel-loader',
33 | options: {
34 | presets: ['@babel/preset-env', '@babel/preset-react']
35 | },
36 | },
37 | exclude: [
38 | path.resolve(__dirname, 'node_modules')
39 | ]
40 | },
41 | {
42 | test: /\.css/i,
43 | use: [
44 | 'style-loader',
45 | 'css-loader'
46 | ],
47 | exclude: [
48 | path.resolve(__dirname, 'node_modules')
49 | ]
50 | },
51 | ]
52 | },
53 | devServer: {
54 | static: {
55 | directory: path.resolve(__dirname)
56 | },
57 | proxy: {
58 | '/bookshelf': 'http://localhost:3000'
59 | },
60 | compress: true,
61 | port: 8080,
62 | historyApiFallback: true
63 | }
64 | }
--------------------------------------------------------------------------------
/extension/src/pages/Panel/Components/Metrics.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Trend from 'react-trend';
3 |
4 | function Metrics () {
5 | const [durationArr, setDuration] = useState(null)
6 | const [time, setTime] = useState(null);
7 | const [avg, setAvg] = useState(null);
8 |
9 | function getStorage () {
10 | chrome.storage.sync.get('request', function(result) {
11 | if (result != {}) {
12 | setDuration(result.request.time)
13 | setTime(result.request.time[result.request.time.length - 1])
14 | let avg = result.request.time.reduce((partialSum, a) => partialSum + a, 0)/result.request.time.length
15 | if (!isNaN(avg)) setAvg(avg.toFixed(2))
16 | }
17 | });
18 | }
19 | chrome.storage.onChanged.addListener(getStorage);
20 | return (
21 |
22 |
Metrics:
23 |
24 |
Last Request Duration: {time} ms
25 |
Average Cache Time: {avg} ms
26 |
27 |
28 |
Speed Graph:
29 | =250 ? Number(`${window.innerHeight}`)-220 : 30}
32 | //width={Number(window.innerWidth / 3)}
33 | className="trend"
34 | data={durationArr && durationArr.map(element => parseInt(element))}
35 | gradient={['#B22222','#DC143C', '#FF7518','#FFAA33']}
36 | radius={0.9}
37 | strokeWidth={2.3}
38 | strokeLinecap={'round'}
39 | />
40 |
41 |
42 | );
43 | };
44 |
45 | export default Metrics;
--------------------------------------------------------------------------------
/__tests__/validateCache.test.js:
--------------------------------------------------------------------------------
1 | import generateKey from '../src/helpers/generateKey';
2 | import clientCache from '../src/flache';
3 | import validateCache from '../src/helpers/validateCache';
4 | import fetchMock from 'jest-fetch-mock';
5 | // require('fake-indexeddb/auto');
6 |
7 | describe('Testing for validateCache function', () => {
8 |
9 | const key = generateKey('https://swapi.dev/api/people', {method: 'GET'});
10 | const obj = {ttl: Date.now() + 5000};
11 | const url = 'https://swapi.dev/api/people';
12 | let cache;
13 |
14 | beforeAll(() => {
15 | cache = new clientCache();
16 | fetchMock.enableMocks();
17 | });
18 |
19 | beforeEach(() => {
20 | fetchMock.resetMocks();
21 | cache.store.clear();
22 | });
23 |
24 | test('validateCache should return data before ttl has expired - tested without store', () => {
25 | return validateCache(key, obj)
26 | .then(data => {
27 | // console.log(data);
28 | expect(data).not.toBe(null);
29 | expect(data.ttl).toBeGreaterThanOrEqual(Date.now());
30 | });
31 | })
32 |
33 | jest.setTimeout(10000);
34 | test('validateCache should return data before ttl has expired - tested with store', async () => {
35 | await cache.flacheRequest(url)
36 | await new Promise((resolve) => setTimeout(resolve, 2000));
37 | return cache.validateCache(key, obj)
38 | .then(data => {
39 | expect(data).not.toBe(null);
40 | });
41 | })
42 |
43 | jest.setTimeout(10000);
44 | test('validateCache should remove item and return null after ttl has expired', async () => {
45 | await cache.flacheRequest(url)
46 | await new Promise((resolve) => setTimeout(resolve, 6000));
47 | return cache.validateCache(key, obj)
48 | .then(data => {
49 | expect(data).toBe(null);
50 | });
51 | })
52 |
53 | });
--------------------------------------------------------------------------------
/src/helpers/parsers.js:
--------------------------------------------------------------------------------
1 | import copyResponse from './copyResponse';
2 |
3 | /**
4 | * Parse response from the server to record the data type
5 | * Middleware pattern calls functions in order until it receives valid data from response
6 | * @param {Response} res - response from server request
7 | * @param {string} res - response from server request
8 | * @return {object} Object containing the resulting data from executing the request in the server and the response detils for later reconstruction.
9 | */
10 |
11 |
12 | async function parseResponse(res) {
13 | const responseCopy = copyResponse(res);
14 | const dataCopy = await parseJSON(res);
15 |
16 | if (!dataCopy) throw new Error('failed to parse data');
17 |
18 | return { response: responseCopy, data: dataCopy };
19 | }
20 |
21 |
22 | async function parseJSON(res) {
23 | try {
24 | const data = { type: 'json', data: await res.json() }
25 | return data
26 | } catch (err) {
27 | console.log(err.message);
28 | return parseText(res);
29 | }
30 | }
31 |
32 |
33 | async function parseText(res) {
34 | try {
35 | const data = { type: 'text', data: await res.text() }
36 | return data
37 | } catch (err) {
38 | console.log(err.message);
39 | return parseBlob(res);
40 | }
41 | }
42 |
43 | // Note these are still in experimental phase
44 | async function parseBlob(res) {
45 | try {
46 | const data = { type: 'blob', data: await res.blob() }
47 | return data
48 | } catch (err) {
49 | console.log(err.message);
50 | return parseArrayBuffer(res);
51 | }
52 | }
53 |
54 | async function parseArrayBuffer(res) {
55 | try {
56 | const data = { type: 'buffer', data: await res.arrayBuffer() };
57 | return data
58 | } catch (err) {
59 | console.log(err.message);
60 | return null;
61 | }
62 | }
63 |
64 | export default parseResponse
--------------------------------------------------------------------------------
/src/helpers/synthResponse.js:
--------------------------------------------------------------------------------
1 | export class SyntheticResponse extends Response {
2 | #redirected;
3 | #type;
4 | #url;
5 | constructor(body, init) {
6 | super(body, init);
7 | this.#redirected = init.redirected || false;
8 | this.#url = init.url || '';
9 | this.#type = init.type || 'default';
10 | }
11 |
12 | get url() {
13 | return this.#url;
14 | }
15 |
16 | get redirected() {
17 | return this.#redirected;
18 | }
19 |
20 | get type() {
21 | return this.#type;
22 | }
23 | }
24 |
25 | export function constructResponse(entry) {
26 | const init = {
27 | ...entry.response,
28 | headers: new Headers(entry.response.headers),
29 | }
30 | /**
31 | * The only properties that acan actually be set via our options are as follow:
32 | * Status
33 | * Status Text
34 | * Headers
35 | *
36 | * Our 'synthetic reposne' overwirtes the getter functions for the url, redirect, and the type to mimic
37 | * the native api as much as possible. Unclear if this could cause serious bugs or not yet.
38 | *
39 | * We should consider what if any of the other properties may interfere with the normal workflow
40 | * a developer might expect, and also how this will impact any other native functions
41 | * that interface with the response obj.
42 | *
43 | * For example - if a user is redirected, but the header is set to manual, the response will not
44 | * be automatically redirected and the user will have to specify a control flow to handle this.
45 | *
46 | * Additionally - in testing I've noticed that the clone method does not work properly either.
47 | */
48 |
49 |
50 | // TO-DO Make sure to parse the data appropriately if it is text v. JSON
51 | const outputResponse = new SyntheticResponse(JSON.stringify(entry.data.data), init)
52 |
53 | return outputResponse;
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/demo/server/controller/booksController.js:
--------------------------------------------------------------------------------
1 | const { request, json } = require('express');
2 | const Book = require('../model/bookModel.js');
3 | const fetch = require('node-fetch');
4 |
5 | const booksController = {};
6 |
7 | booksController.view = (req, res, next) => {
8 | Book.find({}, (err, entries) => {
9 | //console.log(entries)
10 | // entries is an Array of all the entry objects
11 | res.locals.allEntries = entries;
12 | return next();
13 | })
14 | }
15 |
16 | // Get Books From DB by Category
17 | booksController.getBooksByCategory = (req, res, next) => {
18 | const category = req.params.category;
19 | Book.find({category: category}, (err, books) => {
20 | if (err) return next({
21 | log: 'Express Error Handler: booksController.getBooksByCategory',
22 | message: {err: err}
23 | });
24 | // Express error handler if no books are returned
25 | // if (books.length === 0) return next({
26 | // log: 'Express Error Handler: booksController.getBooksByCategory',
27 | // message: {err: 'No Books Found in DB'}
28 | // });
29 | res.locals.books = books;
30 | return next();
31 | })
32 | }
33 |
34 | // Fetch Books From API - USE POSTMAN TO ADD
35 | booksController.fetchBooks = async (req, res, next) => {
36 | res.locals.books = [];
37 | // Add categories to search for books in API
38 | const categories = ['microsoft', 'python', 'javascript', 'ruby', 'mongodb', 'sql', 'cache', 'node', 'it', 'express', 'cpu', 'media', 'computer', 'query'];
39 | // Iterate through categories and fetch books for each category
40 | for (let i = 0; i < categories.length; i++) {
41 | const data = await fetch(`https://api.itbook.store/1.0/search/${categories[i]}`);
42 | const json = await data.json();
43 | const books = json.books;
44 | books.forEach(book => book.category = categories[i]);
45 | res.locals.books.push(...books);
46 | }
47 | return next();
48 | }
49 |
50 | // Add Books to Database From Fetched API
51 | booksController.addFetchedBooks = (req, res, next) => {
52 | Book.insertMany(res.locals.books, (err, data) => {
53 | if (err) return next({
54 | log: 'Express Error Handler: booksController.addFetchedBooks',
55 | message: {err: err}
56 | });
57 | });
58 | return next();
59 | }
60 |
61 | module.exports = booksController;
62 |
63 |
--------------------------------------------------------------------------------
/__tests__/generateKey.test.js:
--------------------------------------------------------------------------------
1 | import generateKey from '../src/helpers/generateKey';
2 |
3 | describe('Testing for generateKey function', () => {
4 |
5 | const defaultOptions = {
6 | method: 'GET',
7 | mode: 'cors',
8 | cache: 'default',
9 | credentials: 'same-origin',
10 | headers: {
11 | 'Content-Type': 'application/x-www-form-urlencoded'
12 | },
13 | redirect: 'follow',
14 | referrerPolicy: 'no-referrer-when-downgrade',
15 | body: null,
16 | };
17 |
18 | test('Generate a unique key using HTTP GET method and default options', () => {
19 | const key = generateKey('https://swapi.dev/api/people', defaultOptions);
20 | expect(key).toMatch('GET/https://swapi.dev/api/people');
21 | })
22 |
23 | test('Generate a unique key using HTTP GET method without default options', () => {
24 | const key = generateKey('https://swapi.dev/api/people', {method: 'GET'});
25 | expect(key).toMatch('GET/https://swapi.dev/api/people');
26 | })
27 |
28 | test('Generate unique key using HTTP POST method', () => {
29 | const key = generateKey('https://swapi.dev/api/people', {method: 'POST', body: {username: 'user', password: 'jio@#fiw924!$#'}});
30 | expect(key.slice(0,6)).toMatch('POST/');
31 | expect(key.length).toBe(66);
32 | })
33 |
34 | test('Hashing of body should return the same number of characters regardless of how large the body is', () => {
35 | const key1 = generateKey('https://swapi.dev/api/people', {method: 'POST', body: {username: 'user', password: 'jio@#fiw924!$#'}});
36 | const key2 = generateKey('https://swapi.dev/api/people', {method: 'POST', body: {username: 'user', password: 'jio@#fiw924!$#', prop1: 'asdfasdf'}});
37 | const key3 = generateKey('https://swapi.dev/api/people', {method: 'POST', body: {username: 'user', password: 'jio@#fiw924!$#', prop1: { prop2: 'asdfasdf'}}});
38 | const key4 = generateKey('https://swapi.dev/api/people', {method: 'POST', body: {username: 'user', password: 'jio@#fiw924!$#', prop1: [{ prop2: 'asdfasdf'}, { prop3: ['hello', 'hi']}]}});
39 | expect(key1.length).toBe(66);
40 | expect(key2.length).toBe(66);
41 | expect(key3.length).toBe(66);
42 | expect(key4.length).toBe(66);
43 | })
44 |
45 | test('Must throw Error if using POST method without a body', () => {
46 | expect(() => {generateKey('https://swapi.dev/api/people', {method: 'POST'})}).toThrow(Error);
47 | expect(() => {generateKey('https://swapi.dev/api/people', {method: 'POST'})}).toThrow('Must include a body with POST request');
48 | })
49 |
50 | })
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flachejs",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "dist/flache.js",
6 | "files": ["/src", "/dist"],
7 | "scripts": {
8 | "test": "jest",
9 | "dev": "webpack serve --open --config webpack.dev.js",
10 | "demo": "concurrently \"cross-env NODE_ENV=development webpack serve --open --config webpack.demo.js\" \"nodemon demo/server/server.js\"",
11 | "build": "webpack --config webpack.prod.js"
12 | },
13 |
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/oslabs-beta/flachejs.git"
17 | },
18 | "keywords": ["flache", "flachejs", "cache", "indexeddb", "localforage", "localstorage", "client-side", "caching", "lru", "ttl", "fetch", "request"],
19 | "contributors": [
20 | {
21 | "name": "Jacob Policano",
22 | "url": "https://github.com/jdpolicano"
23 | },
24 | {
25 | "name": "Jasmair Jaswal",
26 | "url": "https://github.com/twojaytech"
27 | },
28 | {
29 | "name": "Iraj Zuberi",
30 | "url": " https://github.com/izuberi"
31 | },
32 | {
33 | "name": "Vernita Lawren",
34 | "url": "https://github.com/v-law"
35 | }
36 | ],
37 | "license": "ISC",
38 | "bugs": {
39 | "url": "https://github.com/oslabs-beta/flachejs/issues"
40 | },
41 | "homepage": "https://github.com/oslabs-beta/flachejs#readme",
42 | "dependencies": {
43 | "localforage": "^1.10.0",
44 | "md5": "^2.3.0"
45 | },
46 |
47 | "devDependencies": {
48 | "@babel/core": "^7.18.0",
49 | "@babel/preset-env": "^7.18.0",
50 | "@babel/preset-react": "^7.17.12",
51 | "axios": "^0.27.2",
52 | "babel-jest": "^28.1.1",
53 | "babel-loader": "^8.2.5",
54 | "concurrently": "^7.2.1",
55 | "cors": "^2.8.5",
56 | "cross-env": "^7.0.3",
57 | "css-loader": "^6.7.1",
58 | "dotenv": "^16.0.1",
59 | "express": "^4.18.1",
60 | "fake-indexeddb": "^3.1.8",
61 | "file-loader": "^6.2.0",
62 | "html-webpack-plugin": "^5.5.0",
63 | "http": "^0.0.1-security",
64 | "jest": "^28.1.1",
65 | "jest-environment-jsdom": "^28.1.1",
66 | "jest-fetch-mock": "^3.0.3",
67 | "mongoose": "^6.3.4",
68 | "node-fetch": "^2.6.7",
69 | "nodemon": "^2.0.16",
70 | "react": "^18.1.0",
71 | "react-dom": "^18.1.0",
72 | "request": "^2.88.2",
73 | "socket.io": "^4.5.1",
74 | "style-loader": "^3.3.1",
75 | "url-loader": "^4.1.1",
76 | "webpack": "^5.73.0",
77 | "webpack-cli": "^4.10.0",
78 | "webpack-dev-server": "^4.9.2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/extension/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = {
6 | entry: {
7 | devtools: './src/pages/DevTools/', // entry point for chrome dev tool
8 | panel: './src/pages/Panel/' // entry point for react app
9 | },
10 | resolve: {
11 | extensions: ['.js', '.jsx', '.css'],
12 | },
13 | output: {
14 | path: path.join(__dirname, './src/pages/Panel/dist'),
15 | filename: '[name].bundle.js',
16 | clean: true,
17 | },
18 |
19 | devServer: {
20 | historyApiFallback: true,
21 | port: 3333,
22 | static: {
23 | directory: path.join(__dirname, 'src', 'pages', 'panel'),
24 | }
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.js$/,
30 | enforce: 'pre',
31 | use: ['source-map-loader'],
32 | },
33 | {
34 | test: /\.(js|jsx)?$/,
35 | exclude: /node_modules/,
36 | use: {
37 | loader: 'babel-loader',
38 | },
39 | },
40 | {
41 | test: /\.css$/i,
42 | use: ['style-loader', 'css-loader', 'sass-loader']
43 | },
44 | {
45 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
46 | use: ['url-loader'],
47 | },
48 | {
49 | test: /\.html$/,
50 | use: [
51 | {
52 | loader: 'html-loader'
53 | }
54 | ]
55 | }
56 | ],
57 | },
58 | plugins: [
59 | new CopyWebpackPlugin({
60 | patterns: [
61 | {
62 | from: 'src/manifest.json',
63 | to: path.join(__dirname, './src/pages/Panel/dist'),
64 | force: true,
65 | transform: function (content, path) {
66 | // generates the manifest file using the package.json informations
67 | return Buffer.from(
68 | JSON.stringify({
69 | description: process.env.npm_package_description,
70 | version: process.env.npm_package_version,
71 | ...JSON.parse(content.toString()),
72 | })
73 | );
74 | },
75 | },
76 | ],
77 | }),
78 | new HtmlWebpackPlugin({
79 | template: './src/pages/DevTools/index.html',
80 | filename: 'devtools.html',
81 | chunks: ['devtools'],
82 | cache: false,
83 | }),
84 | new HtmlWebpackPlugin({
85 | template: './src/pages/Panel/index.html',
86 | filename: 'panel.html',
87 | chunks: ['panel'],
88 | cache: false,
89 | }),
90 | ],
91 | };
92 |
--------------------------------------------------------------------------------
/src/helpers/flacheRequest.js:
--------------------------------------------------------------------------------
1 | import { constructResponse } from './synthResponse';
2 |
3 | /**
4 | * Function to retreive data from Cache
5 | * @param {string} url - the url to the server request
6 | * @param {object} options - the request body
7 | * @return {object} Object containing the retreived data from the cache
8 | */
9 |
10 | const defaultOptions = {
11 | method: 'GET',
12 | mode: 'cors',
13 | cache: 'default',
14 | credentials: 'same-origin',
15 | headers: {
16 | 'Content-Type': 'application/x-www-form-urlencoded'
17 | },
18 | redirect: 'follow',
19 | referrerPolicy: 'no-referrer-when-downgrade',
20 | body: null,
21 | }
22 |
23 | // TO-DO Add errror handling and potentially some routing.
24 | const flacheRequest = async function (url, options) {
25 |
26 | let start = performance.now()
27 |
28 | options = {
29 | ...defaultOptions,
30 | ...options
31 | };
32 |
33 | let uniqueKey = this.generateKey(url, options);
34 |
35 | /** Check if the cache already contains the response to the given url or exists in cache but is invalid */
36 | const cacheResult = await this.store.getItem(uniqueKey)
37 | .then((entry) => {
38 | if (!entry) return null;
39 | // needs to return data if valid and null if not;
40 | return this.validateCache(uniqueKey, entry);
41 | })
42 | .catch(err => err);
43 |
44 | // what should we do if this throws an err? -> err would indicate that storage is full for write operations
45 | // read operations this would probably indicate an issue with the store itself.
46 |
47 | /** If the cache does not already have stored response from the given url */
48 | if (!cacheResult) {
49 | /** Make a request to the server through the url param to store its response */
50 | const apiResult = await this.getFetchRequest(url, options);
51 |
52 | // if no data returned - should we try again or return an error?
53 | if (!apiResult) {
54 | return null;
55 | }
56 |
57 | /** Apply TTL to object to be stored in cache */
58 | apiResult.ttl = Date.now() + this.ttl;
59 | /** Add to cache */
60 | await this.store.setItem(uniqueKey, apiResult);
61 | // this is where we would potetnially trigger evictions
62 | this.duration = (performance.now() - start).toFixed(2);
63 | this.reqExtension(url, this.duration, 'Miss', this.ttl);
64 | return constructResponse(apiResult);
65 | }
66 |
67 | this.duration = (performance.now() - start).toFixed(2);
68 | this.reqExtension(url, this.duration, 'Hit', -1);
69 | return constructResponse(cacheResult);
70 | };
71 |
72 |
73 | export default flacheRequest;
74 |
--------------------------------------------------------------------------------
/src/flache.js:
--------------------------------------------------------------------------------
1 | import flacheRequest from './helpers/flacheRequest';
2 | import generateKey from './helpers/generateKey';
3 | import validateCache from './helpers/validateCache';
4 | import getFetchRequest from './helpers/serverRequest';
5 | import reqExtension from './helpers/reqExtension';
6 |
7 | /**
8 | * The localforage module
9 | * @external "localforage"
10 | * @see {@link https://www.npmjs.com/package/localforage}
11 | */
12 |
13 | import localforage from 'localforage';
14 |
15 | import FLACHESTORAGE from './flacheStorage';
16 |
17 | localforage.defineDriver(FLACHESTORAGE);
18 |
19 | // import testStorage from './flacheStorageTest';
20 |
21 | const defaultOptions = {
22 | maxCapacity: null, // this is only relevant for local memory at the moment.
23 | ttl: 5000,
24 | duration: null,
25 | config: {
26 | name: 'httpCache',
27 | storeName: 'request_response',
28 | description: 'A cache for client-side http requests',
29 | driver: [
30 | FLACHESTORAGE._driver,
31 | localforage.INDEXEDDB,
32 | localforage.LOCALSTORAGE,
33 | ],
34 | version: 1.0, // this is only relevant if using IndexedDB
35 | }
36 | }
37 |
38 | /** class clientCache provides a container for our db store */
39 | class clientCache {
40 |
41 | /**
42 | * create a clientCache
43 | * @param {object} options - further options for configuring the store
44 | */
45 |
46 | constructor(options = defaultOptions) {
47 |
48 | /** Create store and override the default store options with user-given configurations */
49 | // TO-DO: check if the store exists already and create new store only if it isn't already there.
50 | this.store = localforage.createInstance(({
51 | ...defaultOptions.config,
52 | ...options.config
53 | }))
54 |
55 | /** Create details store */
56 | // TO-DO: same as above.
57 | this.details = localforage.createInstance({
58 | name: 'cacheDetails',
59 | storeName: 'requests',
60 | description: 'A list of past requests',
61 | driver: localforage.INDEXEDDB,
62 | version: 1.0,
63 | })
64 |
65 | /** Apply TTL (time to live) and maxCapacity from user configuration or default */
66 | this.ttl = options.ttl || defaultOptions.ttl;
67 | this.maxCapacity = options.maxCapacity;
68 | this.duration = defaultOptions.duration;
69 | }
70 |
71 | static INDEXEDDB = localforage.INDEXEDDB;
72 | static LOCALSTORAGE = localforage.LOCALSTORAGE;
73 | static MEMORY = 'FLACHESTORAGE';
74 | }
75 |
76 | /** bind helper functions to class clientCache */
77 | clientCache.prototype.flacheRequest = flacheRequest;
78 | clientCache.prototype.generateKey = generateKey;
79 | clientCache.prototype.validateCache = validateCache;
80 | clientCache.prototype.getFetchRequest = getFetchRequest;
81 | clientCache.prototype.reqExtension = reqExtension;
82 |
83 | export default clientCache
--------------------------------------------------------------------------------
/demo/client/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import BookDisplay from "./BookDisplay.jsx";
3 | import flacheClient from '../../../src/flache';
4 |
5 | const store = new flacheClient({
6 | ttl: 60000,
7 | config: {
8 | driver: [
9 | flacheClient.MEMORY,
10 | ]
11 | }
12 | });
13 |
14 | const App = (props) => {
15 | const [books, setBooks] = useState(null);
16 | const [time, setTime] = useState(null);
17 | let bookList = [];
18 |
19 | async function serverReq() {
20 | const query = document.querySelector('#book-category').value
21 | let url;
22 | if (query === 'all') url = "/bookshelf"
23 | else url = '/bookshelf/' + query
24 | let start = performance.now()
25 | await fetch(url)
26 | .then((res) => res.json())
27 | .then((data) => {
28 | console.log('data: ', data);
29 | setTime((performance.now() - start).toFixed(2));
30 | bookList = data.reduce((acc, elem, i) => {
31 | acc.push();
32 | return acc;
33 | }, []);
34 | setBooks(bookList)
35 | })
36 | .catch((err) => console.log('bookshelf fetch error: ', err));
37 | }
38 |
39 | async function flacheReq() {
40 | const query = document.querySelector('#book-category').value;
41 | let url2;
42 | if (query === 'all') url2 = "/bookshelf";
43 | else (url2 = '/bookshelf/' + query);
44 | const books = await store.flacheRequest(url2)
45 | .then(res => {
46 | return res.json()
47 | });
48 | setTime(store.duration);
49 | displayBooks(books);
50 | }
51 |
52 | function displayBooks(data) {
53 | bookList = data.reduce((acc, elem, i) => {
54 | acc.push();
55 | return acc;
56 | }, []);
57 | setBooks(bookList)
58 | }
59 |
60 | return (
61 |
62 |
63 |
64 |
81 |
82 |
83 |
84 |
85 | {time !== null &&
{`Request Duration: ${time} ms`}
}
86 |
87 | {books}
88 |
89 | );
90 | };
91 |
92 | export default App;
--------------------------------------------------------------------------------
/__tests__/synthResponse.test.js:
--------------------------------------------------------------------------------
1 | class SyntheticResponse extends Response {
2 | #redirected;
3 | #type;
4 | #url;
5 | constructor(body, init) {
6 | super(body, init);
7 | this.#redirected = init.redirected || false;
8 | this.#url = init.url || '';
9 | this.#type = init.type || 'default';
10 |
11 | }
12 |
13 | get url() {
14 | return this.#url;
15 | }
16 |
17 | get redirected() {
18 | return this.#redirected;
19 | }
20 |
21 | get type() {
22 | return this.#type;
23 | }
24 | }
25 |
26 |
27 | describe('Synthetic Tests', () => {
28 | let body, init, synthRes, normRes;
29 | beforeAll(() => {
30 | body = [
31 | {
32 | name: 'Jasmair',
33 | occupation: 'Programmer',
34 | company: 'Amazon'
35 | },
36 | {
37 | name: 'Verni',
38 | occupation: 'Programmer',
39 | company: 'Google'
40 | },
41 | {
42 | name: 'Iraj',
43 | occupation: 'Programmer',
44 | company: 'Meta'
45 | }
46 | ],
47 |
48 | init = {
49 | status: 201,
50 | statusText: 'This is a test',
51 | headers: new Headers({'content-type': 'application/json'}),
52 | redirected: true,
53 | type: 'synthetic',
54 | url: 'jest/test'
55 | }
56 | })
57 |
58 | beforeEach(() => {
59 | synthRes = new SyntheticResponse(JSON.stringify(body), init);
60 | normRes = new Response(JSON.stringify(body), init);
61 | })
62 |
63 | describe('Should be able to parse correctly', () => {
64 |
65 | it('Should parse to json', async () => {
66 | const real = await normRes.json();
67 | const fake = await synthRes.json();
68 | expect(fake).toEqual(real);
69 | })
70 | it('Should parse to text', async () => {
71 | const real = await normRes.text();
72 | const fake = await synthRes.text();
73 | expect(fake).toEqual(real);
74 | })
75 | })
76 |
77 | describe('Other Methods should work correctly', () => {
78 |
79 | it('Should clone correctly', async () => {
80 | console.log(synthRes.clone(), true);
81 | expect(true).toEqual(false);
82 | })
83 | })
84 |
85 | describe('Should assign correct properties', () => {
86 | it('Should have a correct url prop', () => {
87 | expect(synthRes.url).toEqual(init.url);
88 | })
89 |
90 | it('Should have a correct redirect prop', () => {
91 | expect(synthRes.redirected).toEqual(init.redirected);
92 | })
93 |
94 | it('Should have a correct type prop', () => {
95 | expect(synthRes.type).toEqual(init.type);
96 | })
97 | })
98 |
99 | describe('Synthetic Response Should mirror Regular Response', () => {
100 |
101 | it('should have the same bodyUsed', () => {
102 | expect(synthRes.bodyUsed).toEqual(normRes.bodyUsed);
103 | })
104 |
105 | it('should have the same headers', () => {
106 | // Testing for equality of keys of Headers object
107 | expect(synthRes.headers).toEqual(normRes.headers);
108 |
109 | // Testing that res header is of type 'Headers'
110 | expect(synthRes.headers instanceof Headers).toEqual(true);
111 |
112 | const synthKeys = []
113 | const realKeys = []
114 |
115 | for (const key of synthRes.headers.keys()) {
116 | synthKeys.push(key);
117 | }
118 |
119 | for (const key of normRes.headers.keys()) {
120 | realKeys.push(key);
121 | }
122 |
123 | // testing that the individual keys are the same
124 | expect(synthKeys).toEqual(realKeys);
125 | })
126 |
127 | it('should have the same status', () => {
128 | expect(synthRes.status).toEqual(normRes.status);
129 | })
130 |
131 | it('should have the same statusText', () => {
132 | expect(synthRes.statusText).toEqual(normRes.statusText);
133 | })
134 | })
135 |
136 | })
--------------------------------------------------------------------------------
/demo/client/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | background-color: #ffe278;
4 | background-image: url("background.png")
5 | }
6 |
7 | html {
8 | height: 100%;
9 | }
10 |
11 | body {
12 | margin: 0;
13 | display: flex;
14 | flex-direction: column;
15 | }
16 |
17 | .header {
18 | flex: 0 1 auto;
19 | }
20 |
21 | .app {
22 | flex: 1 1 auto;
23 | min-height: 0;
24 | }
25 |
26 | main {
27 | height: 100%;
28 | }
29 |
30 | h1,
31 | h2,
32 | h5 {
33 | text-align: center;
34 | padding: 0;
35 | margin: 0;
36 | color: black;
37 | font-family: "Fantasy"
38 | }
39 |
40 | #title {
41 | margin-top: 20px;
42 | }
43 |
44 | .cacheContainer {
45 | margin: 10px;
46 | display: flex;
47 | flex-direction: column;
48 | justify-content: center;
49 | align-items: center;
50 | font-size: 20px;
51 | }
52 |
53 | .cacheDisplay {
54 | text-align: center;
55 | padding: 8px;
56 | border: 3px solid #ee826c;
57 | background-color: rgb(238, 130, 108, 0.5);
58 | /* color: #ffe278; */
59 | font-size: 20px;
60 | border-radius: 12px;
61 | }
62 |
63 | .cacheBtnContainer {
64 | display: grid;
65 | grid-template-columns: 1fr 1fr;
66 | justify-content: center;
67 | align-items: center;
68 |
69 | }
70 |
71 | button {
72 | text-align: center;
73 | background-color: #ee826c;
74 | color: #ffe278;
75 | font-size: 15px;
76 | padding: 8px;
77 | border-radius: 10px;
78 | border: none;
79 | margin: 10px;
80 | }
81 |
82 |
83 | button,
84 | button::after {
85 | font-size: 20px;
86 | font-family: "Fantasy";
87 | background: linear-gradient(45deg, transparent 5%, #FF013C 5%);
88 | border: 0;
89 | color: #fff;
90 | /* letter-spacing: 3px; */
91 | /* line-height: 88px; */
92 | padding: 8px;
93 | box-shadow: 6px 0px 0px #00E6F6;
94 | outline: transparent;
95 | position: relative;
96 | }
97 |
98 | button::after {
99 | --slice-0: inset(50% 50% 50% 50%);
100 | --slice-1: inset(80% -6px 0 0);
101 | --slice-2: inset(50% -6px 30% 0);
102 | --slice-3: inset(10% -6px 85% 0);
103 | --slice-4: inset(40% -6px 43% 0);
104 | --slice-5: inset(80% -6px 5% 0);
105 |
106 | content: 'AVAILABLE NOW';
107 | display: block;
108 | position: absolute;
109 | top: 0;
110 | left: 0;
111 | right: 0;
112 | bottom: 0;
113 | background: linear-gradient(45deg, transparent 3%, #00E6F6 3%, #00E6F6 5%, #FF013C 5%);
114 | text-shadow: -3px -3px 0px #F8F005, 3px 3px 0px #00E6F6;
115 | clip-path: var(--slice-0);
116 | }
117 |
118 | button:hover::after {
119 | animation: 1s glitch;
120 | animation-timing-function: steps(2, end);
121 | }
122 |
123 | @keyframes glitch {
124 | 0% {
125 | clip-path: var(--slice-1);
126 | transform: translate(-20px, -10px);
127 | }
128 |
129 | 10% {
130 | clip-path: var(--slice-3);
131 | transform: translate(10px, 10px);
132 | }
133 |
134 | 20% {
135 | clip-path: var(--slice-1);
136 | transform: translate(-10px, 10px);
137 | }
138 |
139 | 30% {
140 | clip-path: var(--slice-3);
141 | transform: translate(0px, 5px);
142 | }
143 |
144 | 40% {
145 | clip-path: var(--slice-2);
146 | transform: translate(-5px, 0px);
147 | }
148 |
149 | 50% {
150 | clip-path: var(--slice-3);
151 | transform: translate(5px, 0px);
152 | }
153 |
154 | 60% {
155 | clip-path: var(--slice-4);
156 | transform: translate(5px, 10px);
157 | }
158 |
159 | 70% {
160 | clip-path: var(--slice-2);
161 | transform: translate(-10px, 10px);
162 | }
163 |
164 | 80% {
165 | clip-path: var(--slice-5);
166 | transform: translate(20px, -10px);
167 | }
168 |
169 | 90% {
170 | clip-path: var(--slice-1);
171 | transform: translate(-10px, 0px);
172 | }
173 |
174 | 100% {
175 | clip-path: var(--slice-1);
176 | transform: translate(0);
177 | }
178 | }
179 |
180 | .booksContainer {
181 | /* background-color: $mediumbrown; */
182 | /* color: $blue; */
183 | display: grid;
184 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
185 | gap: 10px;
186 | padding-top: 10px;
187 | padding-bottom: 10px;
188 | }
189 |
190 | article {
191 | box-sizing: border-box;
192 | }
193 |
194 | .bookDisplay {
195 | border: 3px solid #ee826c;
196 | background-color: rgb(238, 130, 108, 0.5);
197 | }
198 |
199 | .bookImgContainer {
200 | box-sizing: border-box;
201 | justify-self: center;
202 | align-self: center;
203 | }
204 |
205 | .bookImg {
206 | object-fit: scale-down;
207 | max-height: 200px;
208 | width: auto;
209 | display: block;
210 | margin: auto;
211 | }
212 |
213 | .bookDetail {
214 | display: flex;
215 | font-size: 13px;
216 | }
217 |
218 | .bookLabel {
219 | list-style: none;
220 | margin: 0;
221 | }
--------------------------------------------------------------------------------
/src/flacheStorage.js:
--------------------------------------------------------------------------------
1 | class Node {
2 | constructor(key, val) {
3 | this.key = key;
4 | this.value = val;
5 | this.next = null;
6 | this.prev = null;
7 | }
8 | }
9 |
10 | // Implement the driver here.
11 | const flacheStorage = {
12 | _driver: 'FLACHESTORAGE',
13 | _initStorage: function(capacity, options) {
14 | this.capacity = capacity;
15 | this.cache = new Map();
16 | this.head = null;
17 | this.tail = null;
18 |
19 | if (options) {
20 | for (let key in options) {
21 | this.cache[key] = options[key];
22 | }
23 | }
24 |
25 | return Promise.resolve();
26 | },
27 |
28 | clear: function (callback) {
29 |
30 | return new Promise((resolve, reject) => {
31 | try {
32 | this.cache.clear();
33 | resolve(true);
34 | } catch (err) {
35 | reject(err)
36 | }
37 | })
38 | },
39 |
40 | getItem: function(key) {
41 | return new Promise((resolve, reject) => {
42 | try {
43 | if (this.cache.has(key)) {
44 | let node = this.cache.get(key);
45 | this.moveToFront(node);
46 | return resolve(node.value);
47 | }
48 |
49 | return resolve(null);
50 | } catch (err) {
51 | reject(err);
52 | }
53 | })
54 | },
55 |
56 | iterate: function(iteratorCallback, successCallback) {
57 | // Custom implementation here...
58 | // TO-DO
59 | },
60 |
61 | key: function (n, callback) {
62 | return new Promise((resolve, reject) => {
63 | try {
64 | if (n < 0 || !this.head) return resolve(null);
65 |
66 | if (n === 0) {
67 | if (!callback) resolve(this.head.key);
68 | resolve(callback(this.head.key));
69 | }
70 |
71 | let start = this.head;
72 | let count = 0;
73 |
74 | while (start) {
75 | start = start.next;
76 | count++;
77 | if (count === n) break;
78 | }
79 |
80 | if (count < n) resolve(null);
81 |
82 | if(!callback) resolve(start.key);
83 | resolve(callback(start.key));
84 | } catch (err) {
85 | reject(err);
86 | }
87 | })
88 | },
89 |
90 | keys: function (callback) {
91 | return new Promise((resolve, reject) => {
92 | try {
93 | const keys = this.cache.keys();
94 | if (!callback) resolve([...keys])
95 | resolve(callback([...keys]));
96 | } catch (err) {
97 | reject(err)
98 | }
99 | })
100 | },
101 |
102 | length: function (callback) {
103 | return this.keys().then(keys => {
104 | const length = keys.length;
105 | if (!callback) return length;
106 | return callback(length);
107 | });
108 | },
109 |
110 | removeItem: function(key, callback) {
111 | delete this.cache[key];
112 | return Promise.resolve();
113 | },
114 |
115 | setItem: function(key, value) {
116 | return new Promise((resolve, reject) => {
117 | try {
118 | if (this.cache.has(key)) {
119 | let node = this.cache.get(key);
120 | node.value = value;
121 | this.moveToFront(node);
122 | resolve(value);
123 | }
124 |
125 | let node = new Node(key, value);
126 |
127 | if (this.cache.size === this.capacity) {
128 | this.cache.delete(this.tail.key);
129 | this.deleteNode(this.tail);
130 | }
131 |
132 | this.cache.set(key, node);
133 | this.addFirst(node);
134 | resolve(value);
135 |
136 | } catch (err) {
137 | reject(err);
138 | }
139 | })
140 | },
141 |
142 | moveToFront: function(node) {
143 | this.deleteNode(node);
144 | this.addFirst(node);
145 | return;
146 | },
147 |
148 | deleteNode: function(node) {
149 | let prevNode = node.prev;
150 | let nextNode = node.next;
151 |
152 | if (prevNode) {
153 | prevNode.next = nextNode;
154 | } else {
155 | this.head = nextNode;
156 | }
157 |
158 | if (nextNode) {
159 | nextNode.prev = prevNode;
160 | } else {
161 | this.tail = prevNode;
162 | }
163 |
164 | return;
165 | },
166 |
167 | addFirst: function(node) {
168 | node.next = this.head;
169 | node.prev = null;
170 |
171 | if (this.head) {
172 | this.head.prev = node;
173 | }
174 |
175 | this.head = node;
176 |
177 | if (!this.tail) {
178 | this.tail = node;
179 | }
180 |
181 | return;
182 | },
183 |
184 | printLL: function () {
185 | console.log('This is our Linked List \n', this.head)
186 | }
187 | }
188 |
189 |
190 | export default flacheStorage;
--------------------------------------------------------------------------------
/__tests__/flache.test.js:
--------------------------------------------------------------------------------
1 | import clientCache from '../src/flache';
2 | import fetchMock from 'jest-fetch-mock';
3 |
4 | // setting indexeddb method up for mocking.
5 | require('fake-indexeddb/auto');
6 |
7 | describe('Mock Store Tests', () => {
8 | let cache;
9 | let mockResults;
10 | beforeAll(() => {
11 | cache = new clientCache();
12 | fetchMock.enableMocks();
13 | mockResults = [
14 | {
15 | name: 'Jasmair',
16 | occupation: 'Programmer',
17 | company: 'Amazon'
18 | },
19 | {
20 | name: 'Verni',
21 | occupation: 'Programmer',
22 | company: 'Google'
23 | },
24 | {
25 | name: 'Iraj',
26 | occupation: 'Programmer',
27 | company: 'Meta'
28 | }
29 | ]
30 | })
31 |
32 | beforeEach(() => {
33 | fetchMock.resetMocks();
34 | cache.store.clear();
35 | })
36 |
37 | test('Cache should have a store', async () => {
38 | expect(Object.hasOwn(cache, 'store')).toEqual(true);
39 | })
40 |
41 | test('Cache should have details', async () => {
42 | expect(Object.hasOwn(cache, 'details')).toEqual(true);
43 | })
44 |
45 | test('Cache should have a default ttl of 5000ms', async () => {
46 | expect(Object.hasOwn(cache, 'ttl')).toEqual(true);
47 | expect(cache.ttl).toEqual(5000);
48 | })
49 |
50 | test('Cache ttl should be configurable', async () => {
51 | const newCache = new clientCache({ ttl: 10000 });
52 | expect(Object.hasOwn(newCache, 'ttl')).toEqual(true);
53 | expect(newCache.ttl).toEqual(10000);
54 | })
55 |
56 | test('Cache should have details', async () => {
57 | expect(Object.hasOwn(cache, 'details')).toEqual(true);
58 | })
59 |
60 | test('Cache should have appropriate functions available', async () => {
61 | expect(typeof cache.flacheRequest).toEqual('function');
62 | expect(typeof cache.validateCache).toEqual('function');
63 | expect(typeof cache.getFetchRequest).toEqual('function');
64 | expect(typeof cache.generateKey).toEqual('function');
65 | })
66 |
67 | test('Fetch requests should be stored to cache', async () => {
68 | fetchMock.mockResponseOnce(JSON.stringify(mockResults));
69 | expect(await cache.store.getItem('GET/programmers')).toBe(null);
70 | await cache.flacheRequest('programmers');
71 | expect(await cache.store.getItem('GET/programmers').then(res => res.data)).toEqual(mockResults);
72 | })
73 |
74 | test('Second request should pull from cache instead of API call', async () => {
75 | fetchMock.mockResponseOnce(() => new Promise((resolve) => {
76 | setTimeout(() => resolve(JSON.stringify(mockResults)), 2000);
77 | }))
78 |
79 | const withoutCacheStart = performance.now();
80 | await cache.flacheRequest('programmers');
81 | const withoutCacheFinish = performance.now();
82 |
83 | const withCacheStart = performance.now();
84 | await cache.flacheRequest('programmers');
85 | const withCacheFinish = performance.now();
86 |
87 | const timeWithoutCache = withoutCacheFinish - withoutCacheStart;
88 | const timeWithCache = withCacheFinish - withCacheStart;
89 |
90 | expect(timeWithoutCache > timeWithCache).toEqual(true);
91 | })
92 |
93 | test('Data from cache should be the same as from API call', async () => {
94 | fetchMock.mockResponseOnce(JSON.stringify(mockResults));
95 |
96 | expect(await cache.store.keys().then(keys => keys.length)).toEqual(0);
97 | const firstRequest = await cache.flacheRequest('programmers');
98 | const secondRequest = await cache.flacheRequest('programmers');
99 | expect(firstRequest).toEqual(secondRequest);
100 | expect(await cache.store.keys().then(keys => keys.length)).toEqual(1);
101 | })
102 |
103 | test('Additional Options should be passable to flacheRequests', async () => {
104 | fetchMock.mockResponse(JSON.stringify(mockResults));
105 |
106 | const requestOptions = [{ method: 'POST' }, { method: 'POST', cors: 'same-origin' }, { method: 'GET', cache: 'no-cache' },
107 | { credentials: 'include' }, { method: 'POST', body: JSON.stringify({ user: 'Jake', pw: 'hope this works' }) }];
108 |
109 | for (const option of requestOptions) {
110 | const response = await cache.flacheRequest('programmers', option);
111 | expect(response).toEqual(mockResults);
112 | // clear the cache incase any requests will cached;
113 | await cache.store.clear()
114 | }
115 | })
116 |
117 | test('Should pass failed attempts to the user appropriately', async () => {
118 | const failedResponses = [['An error occured', { status: 500 }], ['An error occured', { status: 503 }],
119 | ['An error occured', { status: 410 }], ['An error occured', { status: 404 }]];
120 |
121 | fetchMock.mockResponses(failedResponses);
122 |
123 | for (let i = 0; i < failedResponses.length; i++) {
124 | // this is a proposed implementation - final output to be determined
125 | expect(() => cache.flacheRequest('programmers')).toThrowError('request not ok');
126 | }
127 | })
128 | })
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FlacheJS
2 |
3 | FlacheJS is an npm library for dealing with client-side caching.
4 |
5 | ## Installation
6 |
7 | ```sh
8 | npm install flachejs
9 | ```
10 |
11 | ## Loading and configuring the module
12 |
13 | ### ES Modules (ESM)
14 |
15 | ```js
16 | import flacheClient from 'flachejs';
17 | ```
18 |
19 | ## Usage
20 |
21 | ### Plain text or HTML
22 |
23 | ```js
24 | import flacheClient from 'flachejs';
25 |
26 | const store = new flacheClient();
27 | const response = await store.flacheRequest('https://github.com/');
28 | const body = await response.text();
29 |
30 | console.log(body);
31 | ```
32 |
33 | ### JSON
34 |
35 | ```js
36 | import flacheClient from 'flachejs';
37 |
38 | const store = new flacheClient();
39 | const response = await store.flacheRequest('https://api.github.com/users/github');
40 | const data= await response.json();
41 |
42 | console.log(data);
43 | ```
44 |
45 | ### Simple Post
46 |
47 | ```js
48 | import flacheClient from 'flachejs';
49 |
50 | const store = new flacheClient();
51 | const response = await store.flacheRequest('https://httpbin.org/post', {method: 'POST', body: 'a=1'});
52 | const data = await response.json();
53 |
54 | console.log(data);
55 | ```
56 |
57 | ### Post with JSON
58 |
59 | ```js
60 | import flacheClient from 'flachejs';
61 |
62 | const body = {a: 1};
63 |
64 | const store = new flacheClient();
65 | const response = await store.flacheRequest('https://httpbin.org/post', {
66 | method: 'post',
67 | body: JSON.stringify(body),
68 | headers: {'Content-Type': 'application/json'}
69 | });
70 | const data = await response.json();
71 |
72 | console.log(data);
73 | ```
74 |
75 | ### Handling Exceptions
76 | Wrapping the fetch function into a `try/catch` block will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError.
77 |
78 | ```js
79 | import flacheClient from 'flachejs';
80 |
81 | const store = new flacheClient();
82 | try {
83 | await store.flacheRequest('https://domain.invalid/');
84 | } catch (error) {
85 | console.log(error);
86 | }
87 | ```
88 |
89 | ## API
90 |
91 | ### flacheClient([options])
92 |
93 | - `options` [Options](#fetch-options) for the cache
94 | - Returns: flache client
95 |
96 | Create a flache client/store.
97 |
98 | #### Options
99 |
100 | The default values are shown after each option key.
101 |
102 | ```js
103 | {
104 | maxCapacity: null, // this is in development
105 | ttl: 5000,
106 | config: {
107 | name: 'httpCache',
108 | storeName: 'request_response',
109 | description: 'A cache for client-side http requests',
110 | driver: [
111 | clientCache.MEMORY, //this is an LRU cache in the local memory
112 | localforage.INDEXEDDB,
113 | localforage.LOCALSTORAGE,
114 | ],
115 | version: 1.0,
116 | }
117 | }
118 | ```
119 |
120 | ### flacheRequest(url[, options])
121 |
122 | - `url` A string representing the URL for fetching
123 | - `options` [Options](#fetch-options) for the HTTP(S) request
124 | - Returns: Promise<[Response](#class-response)>
125 |
126 | Perform an HTTP(S) fetch.
127 |
128 | `url` should be an absolute URL, such as `https://example.com/`. A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) will result in a rejected `Promise`.
129 |
130 | #### Options
131 |
132 | The default values are shown after each option key.
133 |
134 | ```js
135 | {
136 | // These properties are part of the Fetch Standard
137 | method: 'GET',
138 | headers: {}, // Request headers. format is the identical to that accepted by the Headers constructor (see below)
139 | body: null, // Request body. can be null, or a Node.js Readable stream
140 | redirect: 'follow', // Set to `manual` to extract redirect headers, `error` to reject redirect
141 | signal: null, // Pass an instance of AbortSignal to optionally abort requests
142 |
143 | // The following properties are node-fetch extensions
144 | follow: 20, // maximum redirect count. 0 to not follow redirect
145 | compress: true, // support gzip/deflate content encoding. false to disable
146 | size: 0, // maximum response body size in bytes. 0 to disable
147 | agent: null, // http(s).Agent instance or function that returns an instance (see below)
148 | highWaterMark: 16384, // the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource.
149 | insecureHTTPParser: false // Use an insecure HTTP parser that accepts invalid HTTP headers when `true`.
150 | }
151 | ```
152 |
153 | ## Contributing
154 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
155 |
156 | Please make sure to update tests as appropriate.
157 |
158 | ## Team
159 |
160 | - [Iraj Zuberi](https://github.com/izuberi)
161 | - [Jacob Policano](https://github.com/jdpolicano)
162 | - [Jasmair Jaswal](https://github.com/twojaytech)
163 | - [Vernita Lawren](https://github.com/v-law)
164 |
165 | ## License
166 |
167 | FlacheJS is developed under the ISC License
168 |
--------------------------------------------------------------------------------
/dist/flache.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("md5"),require("localforage")):"function"==typeof define&&define.amd?define(["md5","localforage"],t):"object"==typeof exports?exports.flachejs=t(require("md5"),require("localforage")):e.flachejs=t(e.md5,e.localforage)}(self,((e,t)=>(()=>{"use strict";var r={428:e=>{e.exports=t},99:t=>{t.exports=e}},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var s=n[e]={exports:{}};return r[e](s,s.exports,o),s.exports}o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var s={};return(()=>{o.r(s),o.d(s,{default:()=>y});class e extends Response{#e;#t;#r;constructor(e,t){super(e,t),this.#e=t.redirected||!1,this.#r=t.url||"",this.#t=t.type||"default"}get url(){return this.#r}get redirected(){return this.#e}get type(){return this.#t}}function t(t){const r={...t.response,headers:new Headers(t.response.headers)};return new e(JSON.stringify(t.data.data),r)}const r={method:"GET",mode:"cors",cache:"default",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded"},redirect:"follow",referrerPolicy:"no-referrer-when-downgrade",body:null};var n=o(99),i=o.n(n);function a(e){const t=e.entries(),r={};for(const[e,n]of t)r[e]=n;return r}const c=async function(e){const t=function(e,t=["body"]){if(!(e instanceof Response))throw new Error("Not a valid response object");const r={};for(const n in e)t.includes(n)||"function"==typeof e[n]||(r[n]="headers"!==n?e[n]:a(e[n]));return r}(e),r=await async function(e){try{return{type:"json",data:await e.json()}}catch(t){return console.log(t.message),async function(e){try{return{type:"text",data:await e.text()}}catch(t){return console.log(t.message),async function(e){try{return{type:"blob",data:await e.blob()}}catch(t){return console.log(t.message),async function(e){try{return{type:"buffer",data:await e.arrayBuffer()}}catch(e){return console.log(e.message),null}}(e)}}(e)}}(e)}}(e);if(!r)throw new Error("failed to parse data");return{response:t,data:r}};var h=o(428),u=o.n(h);class l{constructor(e,t){this.key=e,this.value=t,this.next=null,this.prev=null}}const d={_driver:"FLACHESTORAGE",_initStorage:function(e,t){if(this.capacity=e,this.cache=new Map,this.head=null,this.tail=null,t)for(let e in t)this.cache[e]=t[e];return Promise.resolve()},clear:function(e){return new Promise(((e,t)=>{try{this.cache.clear(),e(!0)}catch(e){t(e)}}))},getItem:function(e){return new Promise(((t,r)=>{try{if(this.cache.has(e)){let r=this.cache.get(e);return this.moveToFront(r),t(r.value)}return t(null)}catch(e){r(e)}}))},iterate:function(e,t){},key:function(e,t){return new Promise(((r,n)=>{try{if(e<0||!this.head)return r(null);0===e&&(t||r(this.head.key),r(t(this.head.key)));let n=this.head,o=0;for(;n&&(n=n.next,o++,o!==e););o{try{const r=this.cache.keys();e||t([...r]),t(e([...r]))}catch(e){r(e)}}))},length:function(e){return this.keys().then((t=>{const r=t.length;return e?e(r):r}))},removeItem:function(e,t){return delete this.cache[e],Promise.resolve()},setItem:function(e,t){return new Promise(((r,n)=>{try{if(this.cache.has(e)){let n=this.cache.get(e);n.value=t,this.moveToFront(n),r(t)}let n=new l(e,t);this.cache.size===this.capacity&&(this.cache.delete(this.tail.key),this.deleteNode(this.tail)),this.cache.set(e,n),this.addFirst(n),r(t)}catch(e){n(e)}}))},moveToFront:function(e){this.deleteNode(e),this.addFirst(e)},deleteNode:function(e){let t=e.prev,r=e.next;t?t.next=r:this.head=r,r?r.prev=t:this.tail=t},addFirst:function(e){e.next=this.head,e.prev=null,this.head&&(this.head.prev=e),this.head=e,this.tail||(this.tail=e)},printLL:function(){console.log("This is our Linked List \n",this.head)}};u().defineDriver(d);const f={maxCapacity:null,ttl:5e3,duration:null,config:{name:"httpCache",storeName:"request_response",description:"A cache for client-side http requests",driver:[d._driver,u().INDEXEDDB,u().LOCALSTORAGE],version:1}};class p{constructor(e=f){this.store=u().createInstance({...f.config,...e.config}),this.details=u().createInstance({name:"cacheDetails",storeName:"requests",description:"A list of past requests",driver:u().INDEXEDDB,version:1}),this.ttl=e.ttl||f.ttl,this.maxCapacity=e.maxCapacity,this.duration=f.duration}static INDEXEDDB=u().INDEXEDDB;static LOCALSTORAGE=u().LOCALSTORAGE;static MEMORY="FLACHESTORAGE"}p.prototype.flacheRequest=async function(e,n){let o=performance.now();n={...r,...n};let s=this.generateKey(e,n);const i=await this.store.getItem(s).then((e=>e?this.validateCache(s,e):null)).catch((e=>e));if(!i){const r=await this.getFetchRequest(e,n);return r?(r.ttl=Date.now()+this.ttl,await this.store.setItem(s,r),this.duration=(performance.now()-o).toFixed(2),this.reqExtension(e,this.duration,"Miss",this.ttl),t(r)):null}return this.duration=(performance.now()-o).toFixed(2),this.reqExtension(e,this.duration,"Hit",-1),t(i)},p.prototype.generateKey=(e,t)=>{const r=t.method.toUpperCase();if("GET"===r)return`${r}/${e}`;if("POST"===r){if(!Object.hasOwn(t,"body"))throw new Error("Must include a body with POST request");return`${r}/${i()(JSON.stringify(t.body))}/${e}`}},p.prototype.validateCache=async function(e,t){return t.ttlc(e))).catch((e=>(console.log("Fetch error",e.message),e)))},p.prototype.reqExtension=(e,t,r,n)=>{if(chrome&&chrome.runtime&&chrome.runtime.sendMessage){async function o(){let o={requestURL:e,time:t,inCache:r,ttl:n};chrome.runtime.sendMessage("bmkhjogdgeafjdanmhjddmcldejpgaga",o)}o()}};const y=p})(),s})()));
2 | //# sourceMappingURL=flache.min.js.map
--------------------------------------------------------------------------------
/dist/flache.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"flache.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;ACVA;;;;;;;ACAA;;;;;;UCAA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;WCtBA;WACA;WACA;WACA;WACA;WACA,iCAAiC,WAAW;WAC5C;WACA;;;;;WCPA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;;;;;WCAA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;;;;;;;;;;;;ACNO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AAGA;AACA;AACA;AACA;AACA;;;;ACrDoD;;AAEpD;AACA;AACA,WAAW,QAAQ;AACnB,WAAW,QAAQ;AACnB,YAAY,QAAQ;AACpB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW,iBAAiB;AAC5B;AACA;AACA;AACA;AACA,SAAS,iBAAiB;AAC1B;;;AAGA,4DAAe,aAAa,EAAC;;;;;;ACxEP;AACtB;AACA;AACA;AACA,UAAU,QAAQ;AAClB,UAAU,QAAQ;AAClB,WAAW,QAAQ;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,eAAe,OAAO,GAAG,IAAI;AAC7B;AACA;AACA;AACA,eAAe,OAAO,GAAG,8DAAG,4BAA4B,GAAG,IAAI;AAC/D;AACA;;AAEA,0DAAe,WAAW;;ACtB1B;AACA;AACA,WAAW,QAAQ;AACnB,WAAW,QAAQ;AACnB,YAAY,QAAQ;AACpB;AACA;AACA;AACA;AACA;AACA;AACA,IAAI;AACJ;;AAEA,4DAAe,aAAa;;ACd5B;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA,2DAAe;;AChC2B;;AAE1C;AACA;AACA;AACA,WAAW,UAAU;AACrB,WAAW,QAAQ;AACnB,YAAY,QAAQ;AACpB;;;AAGA;AACA,uBAAuB,oBAAY;AACnC;;AAEA;;AAEA,WAAW;AACX;;;AAGA;AACA;AACA,mBAAmB;AACnB;AACA,IAAI;AACJ;AACA;AACA;AACA;;;AAGA;AACA;AACA,mBAAmB;AACnB;AACA,IAAI;AACJ;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,mBAAmB;AACnB;AACA,IAAI;AACJ;AACA;AACA;AACA;;AAEA;AACA;AACA,mBAAmB;AACnB;AACA,IAAI;AACJ;AACA;AACA;AACA;;AAEA,8CAAe;;AC/DuB;;AAEtC;AACA;AACA,WAAW,QAAQ;AACnB,YAAY,QAAQ;AACpB;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,4BAA4B,OAAa;AACzC;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA,oDAAe,eAAe;;AC1B9B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2DAAe,YAAY;;;;;AChB3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA,GAAG;;AAEH;;AAEA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,KAAK;AACL,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,KAAK;AACL,GAAG;;AAEH;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,GAAG;AACH,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,KAAK;AACL,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA,KAAK;AACL,GAAG;;AAEH;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,QAAQ;AACR;AACA;AACA,KAAK;AACL,GAAG;;AAEH;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;;AAEA;AACA;AACA,MAAM;AACN;AACA;;AAEA;AACA;AACA,MAAM;AACN;AACA;;AAEA;AACA,GAAG;;AAEH;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;;;AAGA,wDAAe,aAAa;;AC7LwB;AACJ;AACI;AACE;AACJ;;AAElD;AACA;AACA;AACA,SAAS;AACT;;AAEsC;;AAEM;;AAE5C,2GAAwB,CAAC,iBAAa;;AAEtC;;AAEA,MAAM,qBAAc;AACpB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,yBAAqB;AAC3B,MAAM,0GAAqB;AAC3B,MAAM,6GAAwB;AAC9B;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA,wBAAwB,qBAAc;;AAEtC;AACA;AACA,iBAAiB,6GAA0B;AAC3C,SAAS,qBAAc;AACvB;AACA,KAAK;;AAEL;AACA;AACA,mBAAmB,6GAA0B;AAC7C;AACA;AACA;AACA,cAAc,0GAAqB;AACnC;AACA,KAAK;;AAEL;AACA,8BAA8B,qBAAc;AAC5C;AACA,oBAAoB,qBAAc;AAClC;;AAEA,qBAAqB,0GAAqB;AAC1C,wBAAwB,6GAAwB;AAChD;AACA;;AAEA;AACA,sCAAsC,qBAAa;AACnD,oCAAoC,mBAAW;AAC/C,sCAAsC,qBAAa;AACnD,wCAAwC,aAAe;AACvD,qCAAqC,oBAAY;;AAEjD,6CAAe,a","sources":["webpack://flachejs/webpack/universalModuleDefinition","webpack://flachejs/external umd {\"commonjs\":\"localforage\",\"commonjs2\":\"localforage\",\"amd\":\"localforage\",\"root\":\"localforage\"}","webpack://flachejs/external umd {\"commonjs\":\"md5\",\"commonjs2\":\"md5\",\"amd\":\"md5\",\"root\":\"md5\"}","webpack://flachejs/webpack/bootstrap","webpack://flachejs/webpack/runtime/compat get default export","webpack://flachejs/webpack/runtime/define property getters","webpack://flachejs/webpack/runtime/hasOwnProperty shorthand","webpack://flachejs/webpack/runtime/make namespace object","webpack://flachejs/./src/helpers/synthResponse.js","webpack://flachejs/./src/helpers/flacheRequest.js","webpack://flachejs/./src/helpers/generateKey.js","webpack://flachejs/./src/helpers/validateCache.js","webpack://flachejs/./src/helpers/copyResponse.js","webpack://flachejs/./src/helpers/parsers.js","webpack://flachejs/./src/helpers/serverRequest.js","webpack://flachejs/./src/helpers/reqExtension.js","webpack://flachejs/./src/flacheStorage.js","webpack://flachejs/./src/flache.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"md5\"), require(\"localforage\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"md5\", \"localforage\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"flachejs\"] = factory(require(\"md5\"), require(\"localforage\"));\n\telse\n\t\troot[\"flachejs\"] = factory(root[\"md5\"], root[\"localforage\"]);\n})(self, (__WEBPACK_EXTERNAL_MODULE__99__, __WEBPACK_EXTERNAL_MODULE__428__) => {\nreturn ","module.exports = __WEBPACK_EXTERNAL_MODULE__428__;","module.exports = __WEBPACK_EXTERNAL_MODULE__99__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export class SyntheticResponse extends Response {\n #redirected;\n #type;\n #url;\n constructor(body, init) {\n super(body, init);\n this.#redirected = init.redirected || false;\n this.#url = init.url || '';\n this.#type = init.type || 'default';\n }\n\n get url() {\n return this.#url;\n }\n\n get redirected() {\n return this.#redirected;\n }\n\n get type() {\n return this.#type;\n }\n}\n\nexport function constructResponse(entry) {\n const init = {\n ...entry.response,\n headers: new Headers(entry.response.headers),\n }\n /**\n * The only properties that acan actually be set via our options are as follow: \n * Status\n * Status Text\n * Headers\n * \n * Our 'synthetic reposne' overwirtes the getter functions for the url, redirect, and the type to mimic\n * the native api as much as possible. Unclear if this could cause serious bugs or not yet. \n * \n * We should consider what if any of the other properties may interfere with the normal workflow\n * a developer might expect, and also how this will impact any other native functions\n * that interface with the response obj. \n * \n * For example - if a user is redirected, but the header is set to manual, the response will not\n * be automatically redirected and the user will have to specify a control flow to handle this. \n *\n * Additionally - in testing I've noticed that the clone method does not work properly either. \n */\n\n\n // TO-DO Make sure to parse the data appropriately if it is text v. JSON \n const outputResponse = new SyntheticResponse(JSON.stringify(entry.data.data), init)\n \n return outputResponse;\n}\n\n","import { constructResponse } from './synthResponse';\n\n/**\n * Function to retreive data from Cache\n * @param {string} url - the url to the server request\n * @param {object} options - the request body\n * @return {object} Object containing the retreived data from the cache\n */\n\n const defaultOptions = {\n method: 'GET',\n mode: 'cors',\n cache: 'default',\n credentials: 'same-origin',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n redirect: 'follow',\n referrerPolicy: 'no-referrer-when-downgrade',\n body: null,\n}\n\n// TO-DO Add errror handling and potentially some routing. \nconst flacheRequest = async function (url, options) {\n \n let start = performance.now()\n\n options = {\n ...defaultOptions,\n ...options\n };\n\n let uniqueKey = this.generateKey(url, options);\n\n /** Check if the cache already contains the response to the given url or exists in cache but is invalid */\n const cacheResult = await this.store.getItem(uniqueKey)\n .then((entry) => {\n if (!entry) return null;\n // needs to return data if valid and null if not;\n return this.validateCache(uniqueKey, entry);\n })\n .catch(err => err);\n\n // what should we do if this throws an err? -> err would indicate that storage is full for write operations \n // read operations this would probably indicate an issue with the store itself. \n\n /** If the cache does not already have stored response from the given url */\n if (!cacheResult) {\n /** Make a request to the server through the url param to store its response */\n const apiResult = await this.getFetchRequest(url, options);\n\n // if no data returned - should we try again or return an error? \n if (!apiResult) {\n return null;\n }\n\n /** Apply TTL to object to be stored in cache */\n apiResult.ttl = Date.now() + this.ttl;\n /** Add to cache */\n await this.store.setItem(uniqueKey, apiResult);\n // this is where we would potetnially trigger evictions\n this.duration = (performance.now() - start).toFixed(2);\n this.reqExtension(url, this.duration, 'Miss', this.ttl);\n return constructResponse(apiResult);\n }\n \n this.duration = (performance.now() - start).toFixed(2);\n this.reqExtension(url, this.duration, 'Hit', -1);\n return constructResponse(cacheResult);\n};\n\n\nexport default flacheRequest;\n","import md5 from 'md5';\n \n/**\n* Function that takes in arguments of an HTTP request and returns them as a single unique (hashed) key\n* @param {string} url - request URL\n* @param {object} data - object containing request body such as the HTTP method\n* @return {string} - Hashed key\n**/\n// TO-DO consider including headers in our hashing strategy? If a POST request is made with different headers its conceivable that\n// the expected repsonse would be different; \nconst generateKey = (url, data) => {\n // TO-DO error handling for incorrect method\n const method = data.method.toUpperCase();\n if (method === 'GET') {\n return (`${method}/${url}`);\n }\n if (method === 'POST') {\n if (!Object.hasOwn(data, 'body')) throw new Error('Must include a body with POST request');\n return (`${method}/${md5(JSON.stringify(data.body))}/${url}`);\n }\n}\n\nexport default generateKey;","/**\n * Function that validates the cache\n * @param {string} uniqueKey - hashed key that contains request url and body\n * @param {object} data - response from the server request containing the request's TTL\n * @return {object} object cache value (keys: ttl, data) if in cache and valid, null if not\n */\nasync function validateCache(uniqueKey, data) {\n // check if the item in the store's TTL has passed from the current time of the function call\n if (data.ttl < Date.now()) {\n await this.store.removeItem(uniqueKey);\n return null;\n } else return data;\n}\n\nexport default validateCache;","function copyResponse(res, skip = ['body']) {\n if (!(res instanceof Response)) throw new Error('Not a valid response object'); \n\n const newObj = {};\n for (const key in res) {\n // this is to avoid copying function definitions from the objects prototype; \n // it also checks if we have marked this as a property ot skip. \n if (skip.includes(key) || typeof res[key] === 'function') continue;\n \n //This is to iterate through the headers obj and copy all of the headers returned by the server\n // we will reconstruct this later and recreate the exact same response. \n if (key === 'headers') {\n newObj[key] = copyHeaders(res[key]);\n continue; \n }\n newObj[key] = res[key]; \n }\n return newObj; \n}\n\nfunction copyHeaders(header) {\n const entries = header.entries();\n\n const newObj = {};\n\n for (const [key, value] of entries) {\n newObj[key] = value;\n }\n\n return newObj\n}\n\nexport default copyResponse","import copyResponse from './copyResponse';\n\n/**\n * Parse response from the server to record the data type\n * Middleware pattern calls functions in order until it receives valid data from response\n * @param {Response} res - response from server request\n * @param {string} res - response from server request\n * @return {object} Object containing the resulting data from executing the request in the server and the response detils for later reconstruction. \n */\n\n\nasync function parseResponse(res) {\n const responseCopy = copyResponse(res);\n const dataCopy = await parseJSON(res); \n\n if (!dataCopy) throw new Error('failed to parse data');\n\n return { response: responseCopy, data: dataCopy };\n}\n\n\nasync function parseJSON(res) {\n try {\n const data = { type: 'json', data: await res.json() }\n return data\n } catch (err) {\n console.log(err.message);\n return parseText(res);\n }\n}\n\n\nasync function parseText(res) {\n try {\n const data = { type: 'text', data: await res.text() }\n return data\n } catch (err) {\n console.log(err.message);\n return parseBlob(res);\n }\n}\n\n// Note these are still in experimental phase\nasync function parseBlob(res) {\n try {\n const data = { type: 'blob', data: await res.blob() }\n return data\n } catch (err) {\n console.log(err.message);\n return parseArrayBuffer(res);\n }\n}\n\nasync function parseArrayBuffer(res) {\n try {\n const data = { type: 'buffer', data: await res.arrayBuffer() };\n return data\n } catch (err) {\n console.log(err.message);\n return null;\n }\n}\n\nexport default parseResponse","import parseResponse from './parsers'; \n\n/**\n * Function that makes a Fetch request to the server\n * @param {string} url URL to where fetch request is being made\n * @return {object} Object containing the resulting data from executing the request in the server\n */\n\n/**\n * How will we handle being redirected? \n */\n\nasync function getFetchRequest(url, options) {\n // TO-DO handling headers, response-types, etc for how to parse data; \n let response = await fetch(url, options)\n .then(res => {\n const proxyResponse = parseResponse(res);\n return proxyResponse;\n })\n .catch(err => {\n console.log('Fetch error', err.message);\n return err;\n });\n return response;\n}\n\nexport default getFetchRequest;","const reqExtension = (url, duration, inCache, TTL) => {\n // Send data to our Extension\n if(chrome && chrome.runtime && chrome.runtime.sendMessage) {\n async function sendReq () {\n let aRequest = {\n requestURL: url,\n time: duration, \n inCache: inCache,\n ttl: TTL\n }\n chrome.runtime.sendMessage(\"bmkhjogdgeafjdanmhjddmcldejpgaga\", aRequest);\n } \n sendReq();\n }\n}\n \nexport default reqExtension;","class Node {\n constructor(key, val) {\n this.key = key;\n this.value = val;\n this.next = null;\n this.prev = null;\n }\n}\n\n// Implement the driver here.\nconst flacheStorage = {\n _driver: 'FLACHESTORAGE',\n _initStorage: function(capacity, options) {\n this.capacity = capacity;\n this.cache = new Map(); \n this.head = null;\n this.tail = null;\n\n if (options) {\n for (let key in options) {\n this.cache[key] = options[key];\n }\n }\n\n return Promise.resolve();\n },\n\n clear: function (callback) {\n\n return new Promise((resolve, reject) => { \n try {\n this.cache.clear(); \n resolve(true);\n } catch (err) {\n reject(err)\n }\n })\n },\n \n getItem: function(key) {\n return new Promise((resolve, reject) => {\n try {\n if (this.cache.has(key)) {\n let node = this.cache.get(key);\n this.moveToFront(node);\n return resolve(node.value);\n }\n \n return resolve(null);\n } catch (err) {\n reject(err); \n }\n })\n },\n\n iterate: function(iteratorCallback, successCallback) {\n // Custom implementation here...\n // TO-DO \n }, \n\n key: function (n, callback) {\n return new Promise((resolve, reject) => {\n try {\n if (n < 0 || !this.head) return resolve(null);\n \n if (n === 0) {\n if (!callback) resolve(this.head.key);\n resolve(callback(this.head.key));\n } \n \n let start = this.head; \n let count = 0;\n \n while (start) {\n start = start.next;\n count++;\n if (count === n) break;\n }\n \n if (count < n) resolve(null);\n \n if(!callback) resolve(start.key);\n resolve(callback(start.key));\n } catch (err) {\n reject(err);\n }\n })\n },\n\n keys: function (callback) {\n return new Promise((resolve, reject) => {\n try {\n const keys = this.cache.keys();\n if (!callback) resolve([...keys])\n resolve(callback([...keys]));\n } catch (err) {\n reject(err)\n }\n })\n },\n\n length: function (callback) {\n return this.keys().then(keys => {\n const length = keys.length;\n if (!callback) return length;\n return callback(length);\n });\n },\n\n removeItem: function(key, callback) {\n delete this.cache[key];\n return Promise.resolve();\n },\n\n setItem: function(key, value) {\n return new Promise((resolve, reject) => {\n try {\n if (this.cache.has(key)) {\n let node = this.cache.get(key);\n node.value = value;\n this.moveToFront(node);\n resolve(value);\n }\n \n let node = new Node(key, value);\n \n if (this.cache.size === this.capacity) {\n this.cache.delete(this.tail.key);\n this.deleteNode(this.tail);\n }\n \n this.cache.set(key, node);\n this.addFirst(node);\n resolve(value);\n\n } catch (err) {\n reject(err);\n }\n })\n },\n\n moveToFront: function(node) {\n this.deleteNode(node);\n this.addFirst(node);\n return;\n },\n\n deleteNode: function(node) {\n let prevNode = node.prev;\n let nextNode = node.next;\n\n if (prevNode) {\n prevNode.next = nextNode;\n } else {\n this.head = nextNode;\n }\n\n if (nextNode) {\n nextNode.prev = prevNode;\n } else {\n this.tail = prevNode;\n }\n\n return;\n },\n\n addFirst: function(node) {\n node.next = this.head;\n node.prev = null;\n\n if (this.head) {\n this.head.prev = node;\n }\n\n this.head = node;\n\n if (!this.tail) {\n this.tail = node;\n }\n\n return;\n },\n \n printLL: function () {\n console.log('This is our Linked List \\n', this.head)\n }\n}\n\n\nexport default flacheStorage;","import flacheRequest from './helpers/flacheRequest';\nimport generateKey from './helpers/generateKey';\nimport validateCache from './helpers/validateCache';\nimport getFetchRequest from './helpers/serverRequest';\nimport reqExtension from './helpers/reqExtension';\n\n/**\n * The localforage module\n * @external \"localforage\"\n * @see {@link https://www.npmjs.com/package/localforage}\n */\n\nimport localforage from 'localforage';\n\nimport FLACHESTORAGE from './flacheStorage';\n\nlocalforage.defineDriver(FLACHESTORAGE);\n\n// import testStorage from './flacheStorageTest';\n\nconst defaultOptions = {\n maxCapacity: null, // this is only relevant for local memory at the moment. \n ttl: 5000,\n duration: null,\n config: {\n name: 'httpCache',\n storeName: 'request_response',\n description: 'A cache for client-side http requests',\n driver: [\n FLACHESTORAGE._driver,\n localforage.INDEXEDDB,\n localforage.LOCALSTORAGE,\n ],\n version: 1.0, // this is only relevant if using IndexedDB\n }\n}\n\n/** class clientCache provides a container for our db store */\nclass clientCache {\n\n /**\n * create a clientCache\n * @param {object} options - further options for configuring the store\n */\n \n constructor(options = defaultOptions) {\n\n /** Create store and override the default store options with user-given configurations */\n // TO-DO: check if the store exists already and create new store only if it isn't already there.\n this.store = localforage.createInstance(({\n ...defaultOptions.config,\n ...options.config\n }))\n\n /** Create details store */\n // TO-DO: same as above. \n this.details = localforage.createInstance({\n name: 'cacheDetails',\n storeName: 'requests',\n description: 'A list of past requests',\n driver: localforage.INDEXEDDB,\n version: 1.0,\n })\n\n /** Apply TTL (time to live) and maxCapacity from user configuration or default */\n this.ttl = options.ttl || defaultOptions.ttl;\n this.maxCapacity = options.maxCapacity;\n this.duration = defaultOptions.duration;\n }\n\n static INDEXEDDB = localforage.INDEXEDDB;\n static LOCALSTORAGE = localforage.LOCALSTORAGE;\n static MEMORY = 'FLACHESTORAGE';\n}\n\n/** bind helper functions to class clientCache */\nclientCache.prototype.flacheRequest = flacheRequest;\nclientCache.prototype.generateKey = generateKey;\nclientCache.prototype.validateCache = validateCache;\nclientCache.prototype.getFetchRequest = getFetchRequest;\nclientCache.prototype.reqExtension = reqExtension;\n\nexport default clientCache"],"names":[],"sourceRoot":""}
--------------------------------------------------------------------------------
/dist/flache.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory(require("md5"), require("localforage"));
4 | else if(typeof define === 'function' && define.amd)
5 | define(["md5", "localforage"], factory);
6 | else if(typeof exports === 'object')
7 | exports["flachejs"] = factory(require("md5"), require("localforage"));
8 | else
9 | root["flachejs"] = factory(root["md5"], root["localforage"]);
10 | })(self, (__WEBPACK_EXTERNAL_MODULE__99__, __WEBPACK_EXTERNAL_MODULE__428__) => {
11 | return /******/ (() => { // webpackBootstrap
12 | /******/ "use strict";
13 | /******/ var __webpack_modules__ = ({
14 |
15 | /***/ 428:
16 | /***/ ((module) => {
17 |
18 | module.exports = __WEBPACK_EXTERNAL_MODULE__428__;
19 |
20 | /***/ }),
21 |
22 | /***/ 99:
23 | /***/ ((module) => {
24 |
25 | module.exports = __WEBPACK_EXTERNAL_MODULE__99__;
26 |
27 | /***/ })
28 |
29 | /******/ });
30 | /************************************************************************/
31 | /******/ // The module cache
32 | /******/ var __webpack_module_cache__ = {};
33 | /******/
34 | /******/ // The require function
35 | /******/ function __webpack_require__(moduleId) {
36 | /******/ // Check if module is in cache
37 | /******/ var cachedModule = __webpack_module_cache__[moduleId];
38 | /******/ if (cachedModule !== undefined) {
39 | /******/ return cachedModule.exports;
40 | /******/ }
41 | /******/ // Create a new module (and put it into the cache)
42 | /******/ var module = __webpack_module_cache__[moduleId] = {
43 | /******/ // no module.id needed
44 | /******/ // no module.loaded needed
45 | /******/ exports: {}
46 | /******/ };
47 | /******/
48 | /******/ // Execute the module function
49 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
50 | /******/
51 | /******/ // Return the exports of the module
52 | /******/ return module.exports;
53 | /******/ }
54 | /******/
55 | /************************************************************************/
56 | /******/ /* webpack/runtime/compat get default export */
57 | /******/ (() => {
58 | /******/ // getDefaultExport function for compatibility with non-harmony modules
59 | /******/ __webpack_require__.n = (module) => {
60 | /******/ var getter = module && module.__esModule ?
61 | /******/ () => (module['default']) :
62 | /******/ () => (module);
63 | /******/ __webpack_require__.d(getter, { a: getter });
64 | /******/ return getter;
65 | /******/ };
66 | /******/ })();
67 | /******/
68 | /******/ /* webpack/runtime/define property getters */
69 | /******/ (() => {
70 | /******/ // define getter functions for harmony exports
71 | /******/ __webpack_require__.d = (exports, definition) => {
72 | /******/ for(var key in definition) {
73 | /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
74 | /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
75 | /******/ }
76 | /******/ }
77 | /******/ };
78 | /******/ })();
79 | /******/
80 | /******/ /* webpack/runtime/hasOwnProperty shorthand */
81 | /******/ (() => {
82 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
83 | /******/ })();
84 | /******/
85 | /******/ /* webpack/runtime/make namespace object */
86 | /******/ (() => {
87 | /******/ // define __esModule on exports
88 | /******/ __webpack_require__.r = (exports) => {
89 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
90 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
91 | /******/ }
92 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
93 | /******/ };
94 | /******/ })();
95 | /******/
96 | /************************************************************************/
97 | var __webpack_exports__ = {};
98 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
99 | (() => {
100 | // ESM COMPAT FLAG
101 | __webpack_require__.r(__webpack_exports__);
102 |
103 | // EXPORTS
104 | __webpack_require__.d(__webpack_exports__, {
105 | "default": () => (/* binding */ flache)
106 | });
107 |
108 | ;// CONCATENATED MODULE: ./src/helpers/synthResponse.js
109 | class SyntheticResponse extends Response {
110 | #redirected;
111 | #type;
112 | #url;
113 | constructor(body, init) {
114 | super(body, init);
115 | this.#redirected = init.redirected || false;
116 | this.#url = init.url || '';
117 | this.#type = init.type || 'default';
118 | }
119 |
120 | get url() {
121 | return this.#url;
122 | }
123 |
124 | get redirected() {
125 | return this.#redirected;
126 | }
127 |
128 | get type() {
129 | return this.#type;
130 | }
131 | }
132 |
133 | function constructResponse(entry) {
134 | const init = {
135 | ...entry.response,
136 | headers: new Headers(entry.response.headers),
137 | }
138 | /**
139 | * The only properties that acan actually be set via our options are as follow:
140 | * Status
141 | * Status Text
142 | * Headers
143 | *
144 | * Our 'synthetic reposne' overwirtes the getter functions for the url, redirect, and the type to mimic
145 | * the native api as much as possible. Unclear if this could cause serious bugs or not yet.
146 | *
147 | * We should consider what if any of the other properties may interfere with the normal workflow
148 | * a developer might expect, and also how this will impact any other native functions
149 | * that interface with the response obj.
150 | *
151 | * For example - if a user is redirected, but the header is set to manual, the response will not
152 | * be automatically redirected and the user will have to specify a control flow to handle this.
153 | *
154 | * Additionally - in testing I've noticed that the clone method does not work properly either.
155 | */
156 |
157 |
158 | // TO-DO Make sure to parse the data appropriately if it is text v. JSON
159 | const outputResponse = new SyntheticResponse(JSON.stringify(entry.data.data), init)
160 |
161 | return outputResponse;
162 | }
163 |
164 |
165 | ;// CONCATENATED MODULE: ./src/helpers/flacheRequest.js
166 |
167 |
168 | /**
169 | * Function to retreive data from Cache
170 | * @param {string} url - the url to the server request
171 | * @param {object} options - the request body
172 | * @return {object} Object containing the retreived data from the cache
173 | */
174 |
175 | const defaultOptions = {
176 | method: 'GET',
177 | mode: 'cors',
178 | cache: 'default',
179 | credentials: 'same-origin',
180 | headers: {
181 | 'Content-Type': 'application/x-www-form-urlencoded'
182 | },
183 | redirect: 'follow',
184 | referrerPolicy: 'no-referrer-when-downgrade',
185 | body: null,
186 | }
187 |
188 | // TO-DO Add errror handling and potentially some routing.
189 | const flacheRequest = async function (url, options) {
190 |
191 | let start = performance.now()
192 |
193 | options = {
194 | ...defaultOptions,
195 | ...options
196 | };
197 |
198 | let uniqueKey = this.generateKey(url, options);
199 |
200 | /** Check if the cache already contains the response to the given url or exists in cache but is invalid */
201 | const cacheResult = await this.store.getItem(uniqueKey)
202 | .then((entry) => {
203 | if (!entry) return null;
204 | // needs to return data if valid and null if not;
205 | return this.validateCache(uniqueKey, entry);
206 | })
207 | .catch(err => err);
208 |
209 | // what should we do if this throws an err? -> err would indicate that storage is full for write operations
210 | // read operations this would probably indicate an issue with the store itself.
211 |
212 | /** If the cache does not already have stored response from the given url */
213 | if (!cacheResult) {
214 | /** Make a request to the server through the url param to store its response */
215 | const apiResult = await this.getFetchRequest(url, options);
216 |
217 | // if no data returned - should we try again or return an error?
218 | if (!apiResult) {
219 | return null;
220 | }
221 |
222 | /** Apply TTL to object to be stored in cache */
223 | apiResult.ttl = Date.now() + this.ttl;
224 | /** Add to cache */
225 | await this.store.setItem(uniqueKey, apiResult);
226 | // this is where we would potetnially trigger evictions
227 | this.duration = (performance.now() - start).toFixed(2);
228 | this.reqExtension(url, this.duration, 'Miss', this.ttl);
229 | return constructResponse(apiResult);
230 | }
231 |
232 | this.duration = (performance.now() - start).toFixed(2);
233 | this.reqExtension(url, this.duration, 'Hit', -1);
234 | return constructResponse(cacheResult);
235 | };
236 |
237 |
238 | /* harmony default export */ const helpers_flacheRequest = (flacheRequest);
239 |
240 | // EXTERNAL MODULE: external {"commonjs":"md5","commonjs2":"md5","amd":"md5","root":"md5"}
241 | var external_commonjs_md5_commonjs2_md5_amd_md5_root_md5_ = __webpack_require__(99);
242 | var external_commonjs_md5_commonjs2_md5_amd_md5_root_md5_default = /*#__PURE__*/__webpack_require__.n(external_commonjs_md5_commonjs2_md5_amd_md5_root_md5_);
243 | ;// CONCATENATED MODULE: ./src/helpers/generateKey.js
244 |
245 |
246 | /**
247 | * Function that takes in arguments of an HTTP request and returns them as a single unique (hashed) key
248 | * @param {string} url - request URL
249 | * @param {object} data - object containing request body such as the HTTP method
250 | * @return {string} - Hashed key
251 | **/
252 | // TO-DO consider including headers in our hashing strategy? If a POST request is made with different headers its conceivable that
253 | // the expected repsonse would be different;
254 | const generateKey = (url, data) => {
255 | // TO-DO error handling for incorrect method
256 | const method = data.method.toUpperCase();
257 | if (method === 'GET') {
258 | return (`${method}/${url}`);
259 | }
260 | if (method === 'POST') {
261 | if (!Object.hasOwn(data, 'body')) throw new Error('Must include a body with POST request');
262 | return (`${method}/${external_commonjs_md5_commonjs2_md5_amd_md5_root_md5_default()(JSON.stringify(data.body))}/${url}`);
263 | }
264 | }
265 |
266 | /* harmony default export */ const helpers_generateKey = (generateKey);
267 | ;// CONCATENATED MODULE: ./src/helpers/validateCache.js
268 | /**
269 | * Function that validates the cache
270 | * @param {string} uniqueKey - hashed key that contains request url and body
271 | * @param {object} data - response from the server request containing the request's TTL
272 | * @return {object} object cache value (keys: ttl, data) if in cache and valid, null if not
273 | */
274 | async function validateCache(uniqueKey, data) {
275 | // check if the item in the store's TTL has passed from the current time of the function call
276 | if (data.ttl < Date.now()) {
277 | await this.store.removeItem(uniqueKey);
278 | return null;
279 | } else return data;
280 | }
281 |
282 | /* harmony default export */ const helpers_validateCache = (validateCache);
283 | ;// CONCATENATED MODULE: ./src/helpers/copyResponse.js
284 | function copyResponse(res, skip = ['body']) {
285 | if (!(res instanceof Response)) throw new Error('Not a valid response object');
286 |
287 | const newObj = {};
288 | for (const key in res) {
289 | // this is to avoid copying function definitions from the objects prototype;
290 | // it also checks if we have marked this as a property ot skip.
291 | if (skip.includes(key) || typeof res[key] === 'function') continue;
292 |
293 | //This is to iterate through the headers obj and copy all of the headers returned by the server
294 | // we will reconstruct this later and recreate the exact same response.
295 | if (key === 'headers') {
296 | newObj[key] = copyHeaders(res[key]);
297 | continue;
298 | }
299 | newObj[key] = res[key];
300 | }
301 | return newObj;
302 | }
303 |
304 | function copyHeaders(header) {
305 | const entries = header.entries();
306 |
307 | const newObj = {};
308 |
309 | for (const [key, value] of entries) {
310 | newObj[key] = value;
311 | }
312 |
313 | return newObj
314 | }
315 |
316 | /* harmony default export */ const helpers_copyResponse = (copyResponse);
317 | ;// CONCATENATED MODULE: ./src/helpers/parsers.js
318 |
319 |
320 | /**
321 | * Parse response from the server to record the data type
322 | * Middleware pattern calls functions in order until it receives valid data from response
323 | * @param {Response} res - response from server request
324 | * @param {string} res - response from server request
325 | * @return {object} Object containing the resulting data from executing the request in the server and the response detils for later reconstruction.
326 | */
327 |
328 |
329 | async function parseResponse(res) {
330 | const responseCopy = helpers_copyResponse(res);
331 | const dataCopy = await parseJSON(res);
332 |
333 | if (!dataCopy) throw new Error('failed to parse data');
334 |
335 | return { response: responseCopy, data: dataCopy };
336 | }
337 |
338 |
339 | async function parseJSON(res) {
340 | try {
341 | const data = { type: 'json', data: await res.json() }
342 | return data
343 | } catch (err) {
344 | console.log(err.message);
345 | return parseText(res);
346 | }
347 | }
348 |
349 |
350 | async function parseText(res) {
351 | try {
352 | const data = { type: 'text', data: await res.text() }
353 | return data
354 | } catch (err) {
355 | console.log(err.message);
356 | return parseBlob(res);
357 | }
358 | }
359 |
360 | // Note these are still in experimental phase
361 | async function parseBlob(res) {
362 | try {
363 | const data = { type: 'blob', data: await res.blob() }
364 | return data
365 | } catch (err) {
366 | console.log(err.message);
367 | return parseArrayBuffer(res);
368 | }
369 | }
370 |
371 | async function parseArrayBuffer(res) {
372 | try {
373 | const data = { type: 'buffer', data: await res.arrayBuffer() };
374 | return data
375 | } catch (err) {
376 | console.log(err.message);
377 | return null;
378 | }
379 | }
380 |
381 | /* harmony default export */ const parsers = (parseResponse);
382 | ;// CONCATENATED MODULE: ./src/helpers/serverRequest.js
383 |
384 |
385 | /**
386 | * Function that makes a Fetch request to the server
387 | * @param {string} url URL to where fetch request is being made
388 | * @return {object} Object containing the resulting data from executing the request in the server
389 | */
390 |
391 | /**
392 | * How will we handle being redirected?
393 | */
394 |
395 | async function getFetchRequest(url, options) {
396 | // TO-DO handling headers, response-types, etc for how to parse data;
397 | let response = await fetch(url, options)
398 | .then(res => {
399 | const proxyResponse = parsers(res);
400 | return proxyResponse;
401 | })
402 | .catch(err => {
403 | console.log('Fetch error', err.message);
404 | return err;
405 | });
406 | return response;
407 | }
408 |
409 | /* harmony default export */ const serverRequest = (getFetchRequest);
410 | ;// CONCATENATED MODULE: ./src/helpers/reqExtension.js
411 | const reqExtension = (url, duration, inCache, TTL) => {
412 | // Send data to our Extension
413 | if(chrome && chrome.runtime && chrome.runtime.sendMessage) {
414 | async function sendReq () {
415 | let aRequest = {
416 | requestURL: url,
417 | time: duration,
418 | inCache: inCache,
419 | ttl: TTL
420 | }
421 | chrome.runtime.sendMessage("bmkhjogdgeafjdanmhjddmcldejpgaga", aRequest);
422 | }
423 | sendReq();
424 | }
425 | }
426 |
427 | /* harmony default export */ const helpers_reqExtension = (reqExtension);
428 | // EXTERNAL MODULE: external {"commonjs":"localforage","commonjs2":"localforage","amd":"localforage","root":"localforage"}
429 | var external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_ = __webpack_require__(428);
430 | var external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default = /*#__PURE__*/__webpack_require__.n(external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_);
431 | ;// CONCATENATED MODULE: ./src/flacheStorage.js
432 | class Node {
433 | constructor(key, val) {
434 | this.key = key;
435 | this.value = val;
436 | this.next = null;
437 | this.prev = null;
438 | }
439 | }
440 |
441 | // Implement the driver here.
442 | const flacheStorage = {
443 | _driver: 'FLACHESTORAGE',
444 | _initStorage: function(capacity, options) {
445 | this.capacity = capacity;
446 | this.cache = new Map();
447 | this.head = null;
448 | this.tail = null;
449 |
450 | if (options) {
451 | for (let key in options) {
452 | this.cache[key] = options[key];
453 | }
454 | }
455 |
456 | return Promise.resolve();
457 | },
458 |
459 | clear: function (callback) {
460 |
461 | return new Promise((resolve, reject) => {
462 | try {
463 | this.cache.clear();
464 | resolve(true);
465 | } catch (err) {
466 | reject(err)
467 | }
468 | })
469 | },
470 |
471 | getItem: function(key) {
472 | return new Promise((resolve, reject) => {
473 | try {
474 | if (this.cache.has(key)) {
475 | let node = this.cache.get(key);
476 | this.moveToFront(node);
477 | return resolve(node.value);
478 | }
479 |
480 | return resolve(null);
481 | } catch (err) {
482 | reject(err);
483 | }
484 | })
485 | },
486 |
487 | iterate: function(iteratorCallback, successCallback) {
488 | // Custom implementation here...
489 | // TO-DO
490 | },
491 |
492 | key: function (n, callback) {
493 | return new Promise((resolve, reject) => {
494 | try {
495 | if (n < 0 || !this.head) return resolve(null);
496 |
497 | if (n === 0) {
498 | if (!callback) resolve(this.head.key);
499 | resolve(callback(this.head.key));
500 | }
501 |
502 | let start = this.head;
503 | let count = 0;
504 |
505 | while (start) {
506 | start = start.next;
507 | count++;
508 | if (count === n) break;
509 | }
510 |
511 | if (count < n) resolve(null);
512 |
513 | if(!callback) resolve(start.key);
514 | resolve(callback(start.key));
515 | } catch (err) {
516 | reject(err);
517 | }
518 | })
519 | },
520 |
521 | keys: function (callback) {
522 | return new Promise((resolve, reject) => {
523 | try {
524 | const keys = this.cache.keys();
525 | if (!callback) resolve([...keys])
526 | resolve(callback([...keys]));
527 | } catch (err) {
528 | reject(err)
529 | }
530 | })
531 | },
532 |
533 | length: function (callback) {
534 | return this.keys().then(keys => {
535 | const length = keys.length;
536 | if (!callback) return length;
537 | return callback(length);
538 | });
539 | },
540 |
541 | removeItem: function(key, callback) {
542 | delete this.cache[key];
543 | return Promise.resolve();
544 | },
545 |
546 | setItem: function(key, value) {
547 | return new Promise((resolve, reject) => {
548 | try {
549 | if (this.cache.has(key)) {
550 | let node = this.cache.get(key);
551 | node.value = value;
552 | this.moveToFront(node);
553 | resolve(value);
554 | }
555 |
556 | let node = new Node(key, value);
557 |
558 | if (this.cache.size === this.capacity) {
559 | this.cache.delete(this.tail.key);
560 | this.deleteNode(this.tail);
561 | }
562 |
563 | this.cache.set(key, node);
564 | this.addFirst(node);
565 | resolve(value);
566 |
567 | } catch (err) {
568 | reject(err);
569 | }
570 | })
571 | },
572 |
573 | moveToFront: function(node) {
574 | this.deleteNode(node);
575 | this.addFirst(node);
576 | return;
577 | },
578 |
579 | deleteNode: function(node) {
580 | let prevNode = node.prev;
581 | let nextNode = node.next;
582 |
583 | if (prevNode) {
584 | prevNode.next = nextNode;
585 | } else {
586 | this.head = nextNode;
587 | }
588 |
589 | if (nextNode) {
590 | nextNode.prev = prevNode;
591 | } else {
592 | this.tail = prevNode;
593 | }
594 |
595 | return;
596 | },
597 |
598 | addFirst: function(node) {
599 | node.next = this.head;
600 | node.prev = null;
601 |
602 | if (this.head) {
603 | this.head.prev = node;
604 | }
605 |
606 | this.head = node;
607 |
608 | if (!this.tail) {
609 | this.tail = node;
610 | }
611 |
612 | return;
613 | },
614 |
615 | printLL: function () {
616 | console.log('This is our Linked List \n', this.head)
617 | }
618 | }
619 |
620 |
621 | /* harmony default export */ const src_flacheStorage = (flacheStorage);
622 | ;// CONCATENATED MODULE: ./src/flache.js
623 |
624 |
625 |
626 |
627 |
628 |
629 | /**
630 | * The localforage module
631 | * @external "localforage"
632 | * @see {@link https://www.npmjs.com/package/localforage}
633 | */
634 |
635 |
636 |
637 |
638 |
639 | external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default().defineDriver(src_flacheStorage);
640 |
641 | // import testStorage from './flacheStorageTest';
642 |
643 | const flache_defaultOptions = {
644 | maxCapacity: null, // this is only relevant for local memory at the moment.
645 | ttl: 5000,
646 | duration: null,
647 | config: {
648 | name: 'httpCache',
649 | storeName: 'request_response',
650 | description: 'A cache for client-side http requests',
651 | driver: [
652 | src_flacheStorage._driver,
653 | (external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default()).INDEXEDDB,
654 | (external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default()).LOCALSTORAGE,
655 | ],
656 | version: 1.0, // this is only relevant if using IndexedDB
657 | }
658 | }
659 |
660 | /** class clientCache provides a container for our db store */
661 | class clientCache {
662 |
663 | /**
664 | * create a clientCache
665 | * @param {object} options - further options for configuring the store
666 | */
667 |
668 | constructor(options = flache_defaultOptions) {
669 |
670 | /** Create store and override the default store options with user-given configurations */
671 | // TO-DO: check if the store exists already and create new store only if it isn't already there.
672 | this.store = external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default().createInstance(({
673 | ...flache_defaultOptions.config,
674 | ...options.config
675 | }))
676 |
677 | /** Create details store */
678 | // TO-DO: same as above.
679 | this.details = external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default().createInstance({
680 | name: 'cacheDetails',
681 | storeName: 'requests',
682 | description: 'A list of past requests',
683 | driver: (external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default()).INDEXEDDB,
684 | version: 1.0,
685 | })
686 |
687 | /** Apply TTL (time to live) and maxCapacity from user configuration or default */
688 | this.ttl = options.ttl || flache_defaultOptions.ttl;
689 | this.maxCapacity = options.maxCapacity;
690 | this.duration = flache_defaultOptions.duration;
691 | }
692 |
693 | static INDEXEDDB = (external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default()).INDEXEDDB;
694 | static LOCALSTORAGE = (external_commonjs_localforage_commonjs2_localforage_amd_localforage_root_localforage_default()).LOCALSTORAGE;
695 | static MEMORY = 'FLACHESTORAGE';
696 | }
697 |
698 | /** bind helper functions to class clientCache */
699 | clientCache.prototype.flacheRequest = helpers_flacheRequest;
700 | clientCache.prototype.generateKey = helpers_generateKey;
701 | clientCache.prototype.validateCache = helpers_validateCache;
702 | clientCache.prototype.getFetchRequest = serverRequest;
703 | clientCache.prototype.reqExtension = helpers_reqExtension;
704 |
705 | /* harmony default export */ const flache = (clientCache);
706 | })();
707 |
708 | /******/ return __webpack_exports__;
709 | /******/ })()
710 | ;
711 | });
712 | //# sourceMappingURL=flache.js.map
--------------------------------------------------------------------------------
/dist/flache.min.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"flache.min.js","mappings":"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","sources":["webpack://flachejs/webpack/universalModuleDefinition","webpack://flachejs/external umd {\"commonjs\":\"localforage\",\"commonjs2\":\"localforage\",\"amd\":\"localforage\",\"root\":\"localforage\"}","webpack://flachejs/external umd {\"commonjs\":\"md5\",\"commonjs2\":\"md5\",\"amd\":\"md5\",\"root\":\"md5\"}","webpack://flachejs/webpack/bootstrap","webpack://flachejs/webpack/runtime/compat get default export","webpack://flachejs/webpack/runtime/define property getters","webpack://flachejs/webpack/runtime/hasOwnProperty shorthand","webpack://flachejs/webpack/runtime/make namespace object","webpack://flachejs/./src/helpers/synthResponse.js","webpack://flachejs/./src/helpers/flacheRequest.js","webpack://flachejs/./src/helpers/copyResponse.js","webpack://flachejs/./src/helpers/parsers.js","webpack://flachejs/./src/flacheStorage.js","webpack://flachejs/./src/flache.js","webpack://flachejs/./src/helpers/generateKey.js","webpack://flachejs/./src/helpers/validateCache.js","webpack://flachejs/./src/helpers/serverRequest.js","webpack://flachejs/./src/helpers/reqExtension.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"md5\"), require(\"localforage\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"md5\", \"localforage\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"flachejs\"] = factory(require(\"md5\"), require(\"localforage\"));\n\telse\n\t\troot[\"flachejs\"] = factory(root[\"md5\"], root[\"localforage\"]);\n})(self, (__WEBPACK_EXTERNAL_MODULE__99__, __WEBPACK_EXTERNAL_MODULE__428__) => {\nreturn ","module.exports = __WEBPACK_EXTERNAL_MODULE__428__;","module.exports = __WEBPACK_EXTERNAL_MODULE__99__;","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export class SyntheticResponse extends Response {\n #redirected;\n #type;\n #url;\n constructor(body, init) {\n super(body, init);\n this.#redirected = init.redirected || false;\n this.#url = init.url || '';\n this.#type = init.type || 'default';\n }\n\n get url() {\n return this.#url;\n }\n\n get redirected() {\n return this.#redirected;\n }\n\n get type() {\n return this.#type;\n }\n}\n\nexport function constructResponse(entry) {\n const init = {\n ...entry.response,\n headers: new Headers(entry.response.headers),\n }\n /**\n * The only properties that acan actually be set via our options are as follow: \n * Status\n * Status Text\n * Headers\n * \n * Our 'synthetic reposne' overwirtes the getter functions for the url, redirect, and the type to mimic\n * the native api as much as possible. Unclear if this could cause serious bugs or not yet. \n * \n * We should consider what if any of the other properties may interfere with the normal workflow\n * a developer might expect, and also how this will impact any other native functions\n * that interface with the response obj. \n * \n * For example - if a user is redirected, but the header is set to manual, the response will not\n * be automatically redirected and the user will have to specify a control flow to handle this. \n *\n * Additionally - in testing I've noticed that the clone method does not work properly either. \n */\n\n\n // TO-DO Make sure to parse the data appropriately if it is text v. JSON \n const outputResponse = new SyntheticResponse(JSON.stringify(entry.data.data), init)\n \n return outputResponse;\n}\n\n","import { constructResponse } from './synthResponse';\n\n/**\n * Function to retreive data from Cache\n * @param {string} url - the url to the server request\n * @param {object} options - the request body\n * @return {object} Object containing the retreived data from the cache\n */\n\n const defaultOptions = {\n method: 'GET',\n mode: 'cors',\n cache: 'default',\n credentials: 'same-origin',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n redirect: 'follow',\n referrerPolicy: 'no-referrer-when-downgrade',\n body: null,\n}\n\n// TO-DO Add errror handling and potentially some routing. \nconst flacheRequest = async function (url, options) {\n \n let start = performance.now()\n\n options = {\n ...defaultOptions,\n ...options\n };\n\n let uniqueKey = this.generateKey(url, options);\n\n /** Check if the cache already contains the response to the given url or exists in cache but is invalid */\n const cacheResult = await this.store.getItem(uniqueKey)\n .then((entry) => {\n if (!entry) return null;\n // needs to return data if valid and null if not;\n return this.validateCache(uniqueKey, entry);\n })\n .catch(err => err);\n\n // what should we do if this throws an err? -> err would indicate that storage is full for write operations \n // read operations this would probably indicate an issue with the store itself. \n\n /** If the cache does not already have stored response from the given url */\n if (!cacheResult) {\n /** Make a request to the server through the url param to store its response */\n const apiResult = await this.getFetchRequest(url, options);\n\n // if no data returned - should we try again or return an error? \n if (!apiResult) {\n return null;\n }\n\n /** Apply TTL to object to be stored in cache */\n apiResult.ttl = Date.now() + this.ttl;\n /** Add to cache */\n await this.store.setItem(uniqueKey, apiResult);\n // this is where we would potetnially trigger evictions\n this.duration = (performance.now() - start).toFixed(2);\n this.reqExtension(url, this.duration, 'Miss', this.ttl);\n return constructResponse(apiResult);\n }\n \n this.duration = (performance.now() - start).toFixed(2);\n this.reqExtension(url, this.duration, 'Hit', -1);\n return constructResponse(cacheResult);\n};\n\n\nexport default flacheRequest;\n","function copyResponse(res, skip = ['body']) {\n if (!(res instanceof Response)) throw new Error('Not a valid response object'); \n\n const newObj = {};\n for (const key in res) {\n // this is to avoid copying function definitions from the objects prototype; \n // it also checks if we have marked this as a property ot skip. \n if (skip.includes(key) || typeof res[key] === 'function') continue;\n \n //This is to iterate through the headers obj and copy all of the headers returned by the server\n // we will reconstruct this later and recreate the exact same response. \n if (key === 'headers') {\n newObj[key] = copyHeaders(res[key]);\n continue; \n }\n newObj[key] = res[key]; \n }\n return newObj; \n}\n\nfunction copyHeaders(header) {\n const entries = header.entries();\n\n const newObj = {};\n\n for (const [key, value] of entries) {\n newObj[key] = value;\n }\n\n return newObj\n}\n\nexport default copyResponse","import copyResponse from './copyResponse';\n\n/**\n * Parse response from the server to record the data type\n * Middleware pattern calls functions in order until it receives valid data from response\n * @param {Response} res - response from server request\n * @param {string} res - response from server request\n * @return {object} Object containing the resulting data from executing the request in the server and the response detils for later reconstruction. \n */\n\n\nasync function parseResponse(res) {\n const responseCopy = copyResponse(res);\n const dataCopy = await parseJSON(res); \n\n if (!dataCopy) throw new Error('failed to parse data');\n\n return { response: responseCopy, data: dataCopy };\n}\n\n\nasync function parseJSON(res) {\n try {\n const data = { type: 'json', data: await res.json() }\n return data\n } catch (err) {\n console.log(err.message);\n return parseText(res);\n }\n}\n\n\nasync function parseText(res) {\n try {\n const data = { type: 'text', data: await res.text() }\n return data\n } catch (err) {\n console.log(err.message);\n return parseBlob(res);\n }\n}\n\n// Note these are still in experimental phase\nasync function parseBlob(res) {\n try {\n const data = { type: 'blob', data: await res.blob() }\n return data\n } catch (err) {\n console.log(err.message);\n return parseArrayBuffer(res);\n }\n}\n\nasync function parseArrayBuffer(res) {\n try {\n const data = { type: 'buffer', data: await res.arrayBuffer() };\n return data\n } catch (err) {\n console.log(err.message);\n return null;\n }\n}\n\nexport default parseResponse","class Node {\n constructor(key, val) {\n this.key = key;\n this.value = val;\n this.next = null;\n this.prev = null;\n }\n}\n\n// Implement the driver here.\nconst flacheStorage = {\n _driver: 'FLACHESTORAGE',\n _initStorage: function(capacity, options) {\n this.capacity = capacity;\n this.cache = new Map(); \n this.head = null;\n this.tail = null;\n\n if (options) {\n for (let key in options) {\n this.cache[key] = options[key];\n }\n }\n\n return Promise.resolve();\n },\n\n clear: function (callback) {\n\n return new Promise((resolve, reject) => { \n try {\n this.cache.clear(); \n resolve(true);\n } catch (err) {\n reject(err)\n }\n })\n },\n \n getItem: function(key) {\n return new Promise((resolve, reject) => {\n try {\n if (this.cache.has(key)) {\n let node = this.cache.get(key);\n this.moveToFront(node);\n return resolve(node.value);\n }\n \n return resolve(null);\n } catch (err) {\n reject(err); \n }\n })\n },\n\n iterate: function(iteratorCallback, successCallback) {\n // Custom implementation here...\n // TO-DO \n }, \n\n key: function (n, callback) {\n return new Promise((resolve, reject) => {\n try {\n if (n < 0 || !this.head) return resolve(null);\n \n if (n === 0) {\n if (!callback) resolve(this.head.key);\n resolve(callback(this.head.key));\n } \n \n let start = this.head; \n let count = 0;\n \n while (start) {\n start = start.next;\n count++;\n if (count === n) break;\n }\n \n if (count < n) resolve(null);\n \n if(!callback) resolve(start.key);\n resolve(callback(start.key));\n } catch (err) {\n reject(err);\n }\n })\n },\n\n keys: function (callback) {\n return new Promise((resolve, reject) => {\n try {\n const keys = this.cache.keys();\n if (!callback) resolve([...keys])\n resolve(callback([...keys]));\n } catch (err) {\n reject(err)\n }\n })\n },\n\n length: function (callback) {\n return this.keys().then(keys => {\n const length = keys.length;\n if (!callback) return length;\n return callback(length);\n });\n },\n\n removeItem: function(key, callback) {\n delete this.cache[key];\n return Promise.resolve();\n },\n\n setItem: function(key, value) {\n return new Promise((resolve, reject) => {\n try {\n if (this.cache.has(key)) {\n let node = this.cache.get(key);\n node.value = value;\n this.moveToFront(node);\n resolve(value);\n }\n \n let node = new Node(key, value);\n \n if (this.cache.size === this.capacity) {\n this.cache.delete(this.tail.key);\n this.deleteNode(this.tail);\n }\n \n this.cache.set(key, node);\n this.addFirst(node);\n resolve(value);\n\n } catch (err) {\n reject(err);\n }\n })\n },\n\n moveToFront: function(node) {\n this.deleteNode(node);\n this.addFirst(node);\n return;\n },\n\n deleteNode: function(node) {\n let prevNode = node.prev;\n let nextNode = node.next;\n\n if (prevNode) {\n prevNode.next = nextNode;\n } else {\n this.head = nextNode;\n }\n\n if (nextNode) {\n nextNode.prev = prevNode;\n } else {\n this.tail = prevNode;\n }\n\n return;\n },\n\n addFirst: function(node) {\n node.next = this.head;\n node.prev = null;\n\n if (this.head) {\n this.head.prev = node;\n }\n\n this.head = node;\n\n if (!this.tail) {\n this.tail = node;\n }\n\n return;\n },\n \n printLL: function () {\n console.log('This is our Linked List \\n', this.head)\n }\n}\n\n\nexport default flacheStorage;","import flacheRequest from './helpers/flacheRequest';\nimport generateKey from './helpers/generateKey';\nimport validateCache from './helpers/validateCache';\nimport getFetchRequest from './helpers/serverRequest';\nimport reqExtension from './helpers/reqExtension';\n\n/**\n * The localforage module\n * @external \"localforage\"\n * @see {@link https://www.npmjs.com/package/localforage}\n */\n\nimport localforage from 'localforage';\n\nimport FLACHESTORAGE from './flacheStorage';\n\nlocalforage.defineDriver(FLACHESTORAGE);\n\n// import testStorage from './flacheStorageTest';\n\nconst defaultOptions = {\n maxCapacity: null, // this is only relevant for local memory at the moment. \n ttl: 5000,\n duration: null,\n config: {\n name: 'httpCache',\n storeName: 'request_response',\n description: 'A cache for client-side http requests',\n driver: [\n FLACHESTORAGE._driver,\n localforage.INDEXEDDB,\n localforage.LOCALSTORAGE,\n ],\n version: 1.0, // this is only relevant if using IndexedDB\n }\n}\n\n/** class clientCache provides a container for our db store */\nclass clientCache {\n\n /**\n * create a clientCache\n * @param {object} options - further options for configuring the store\n */\n \n constructor(options = defaultOptions) {\n\n /** Create store and override the default store options with user-given configurations */\n // TO-DO: check if the store exists already and create new store only if it isn't already there.\n this.store = localforage.createInstance(({\n ...defaultOptions.config,\n ...options.config\n }))\n\n /** Create details store */\n // TO-DO: same as above. \n this.details = localforage.createInstance({\n name: 'cacheDetails',\n storeName: 'requests',\n description: 'A list of past requests',\n driver: localforage.INDEXEDDB,\n version: 1.0,\n })\n\n /** Apply TTL (time to live) and maxCapacity from user configuration or default */\n this.ttl = options.ttl || defaultOptions.ttl;\n this.maxCapacity = options.maxCapacity;\n this.duration = defaultOptions.duration;\n }\n\n static INDEXEDDB = localforage.INDEXEDDB;\n static LOCALSTORAGE = localforage.LOCALSTORAGE;\n static MEMORY = 'FLACHESTORAGE';\n}\n\n/** bind helper functions to class clientCache */\nclientCache.prototype.flacheRequest = flacheRequest;\nclientCache.prototype.generateKey = generateKey;\nclientCache.prototype.validateCache = validateCache;\nclientCache.prototype.getFetchRequest = getFetchRequest;\nclientCache.prototype.reqExtension = reqExtension;\n\nexport default clientCache","import md5 from 'md5';\n \n/**\n* Function that takes in arguments of an HTTP request and returns them as a single unique (hashed) key\n* @param {string} url - request URL\n* @param {object} data - object containing request body such as the HTTP method\n* @return {string} - Hashed key\n**/\n// TO-DO consider including headers in our hashing strategy? If a POST request is made with different headers its conceivable that\n// the expected repsonse would be different; \nconst generateKey = (url, data) => {\n // TO-DO error handling for incorrect method\n const method = data.method.toUpperCase();\n if (method === 'GET') {\n return (`${method}/${url}`);\n }\n if (method === 'POST') {\n if (!Object.hasOwn(data, 'body')) throw new Error('Must include a body with POST request');\n return (`${method}/${md5(JSON.stringify(data.body))}/${url}`);\n }\n}\n\nexport default generateKey;","/**\n * Function that validates the cache\n * @param {string} uniqueKey - hashed key that contains request url and body\n * @param {object} data - response from the server request containing the request's TTL\n * @return {object} object cache value (keys: ttl, data) if in cache and valid, null if not\n */\nasync function validateCache(uniqueKey, data) {\n // check if the item in the store's TTL has passed from the current time of the function call\n if (data.ttl < Date.now()) {\n await this.store.removeItem(uniqueKey);\n return null;\n } else return data;\n}\n\nexport default validateCache;","import parseResponse from './parsers'; \n\n/**\n * Function that makes a Fetch request to the server\n * @param {string} url URL to where fetch request is being made\n * @return {object} Object containing the resulting data from executing the request in the server\n */\n\n/**\n * How will we handle being redirected? \n */\n\nasync function getFetchRequest(url, options) {\n // TO-DO handling headers, response-types, etc for how to parse data; \n let response = await fetch(url, options)\n .then(res => {\n const proxyResponse = parseResponse(res);\n return proxyResponse;\n })\n .catch(err => {\n console.log('Fetch error', err.message);\n return err;\n });\n return response;\n}\n\nexport default getFetchRequest;","const reqExtension = (url, duration, inCache, TTL) => {\n // Send data to our Extension\n if(chrome && chrome.runtime && chrome.runtime.sendMessage) {\n async function sendReq () {\n let aRequest = {\n requestURL: url,\n time: duration, \n inCache: inCache,\n ttl: TTL\n }\n chrome.runtime.sendMessage(\"bmkhjogdgeafjdanmhjddmcldejpgaga\", aRequest);\n } \n sendReq();\n }\n}\n \nexport default reqExtension;"],"names":["root","factory","exports","module","require","define","amd","self","__WEBPACK_EXTERNAL_MODULE__99__","__WEBPACK_EXTERNAL_MODULE__428__","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","n","getter","__esModule","d","a","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","r","Symbol","toStringTag","value","SyntheticResponse","Response","constructor","body","init","super","this","redirected","url","type","constructResponse","entry","response","headers","Headers","JSON","stringify","data","defaultOptions","method","mode","cache","credentials","redirect","referrerPolicy","copyHeaders","header","entries","newObj","async","res","responseCopy","skip","Error","includes","dataCopy","json","err","console","log","message","text","blob","arrayBuffer","parseArrayBuffer","parseBlob","parseText","parseJSON","Node","val","next","prev","_driver","_initStorage","capacity","options","Map","head","tail","Promise","resolve","clear","callback","reject","getItem","has","node","moveToFront","iterate","iteratorCallback","successCallback","start","count","keys","length","then","removeItem","setItem","size","delete","deleteNode","set","addFirst","prevNode","nextNode","printLL","maxCapacity","ttl","duration","config","name","storeName","description","driver","version","clientCache","store","details","static","flacheRequest","performance","now","uniqueKey","generateKey","cacheResult","validateCache","catch","apiResult","getFetchRequest","Date","toFixed","reqExtension","toUpperCase","hasOwn","fetch","inCache","TTL","chrome","runtime","sendMessage","sendReq","aRequest","requestURL","time"],"sourceRoot":""}
--------------------------------------------------------------------------------