├── 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 |
8 |

flache demo

9 |

library

10 |
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 | 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 | 29 | 30 | 31 | {req} 32 | 33 |
URLTime (ms)Cache
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":""} --------------------------------------------------------------------------------