├── .travis.yml ├── app ├── adapter │ └── global.js ├── configuration │ ├── default.js │ └── main.js ├── main.js ├── cache │ ├── check-freshness.js │ ├── create-key.js │ ├── set-item.js │ ├── get-item.js │ ├── create-item.js │ └── calculate-validity.js ├── utils │ └── throw-invalid.js ├── pattern-match │ └── main.js ├── header │ ├── valid-content-type.js │ ├── utils │ │ ├── get-value.js │ │ └── check-value.js │ ├── valid-cache-control.js │ └── valid-status-code.js ├── facade │ ├── sessionstorage.js │ ├── localstorage__async.js │ └── localstorage.js └── api │ ├── before.js │ ├── after.js │ └── main.js ├── .eslintignore ├── .babelrc ├── docker-build.sh ├── docker ├── node-shell │ └── Dockerfile └── node-modules │ └── Dockerfile ├── .eslintrc.js ├── .editorconfig ├── .mocharc.js ├── .gitignore ├── LICENSE.md ├── webpack.config.js ├── test ├── common.js ├── cache │ ├── create-key.spec.js │ ├── set-item.spec.js │ ├── check-freshness.spec.js │ ├── get-item.spec.js │ ├── calculate-validity.spec.js │ └── create-item.spec.js ├── sanity.spec.js ├── main.spec.js ├── header │ ├── valid-status-code.spec.js │ ├── valid-content-type.spec.js │ ├── valid-cache-control.spec.js │ └── utils │ │ ├── get-value.spec.js │ │ └── check-value.spec.js ├── pattern-match │ └── match.spec.js ├── utils │ └── throw-invalid.spec.js ├── api │ ├── main.spec.js │ ├── before.spec.js │ └── after.spec.js ├── facade │ ├── sessionstorage.spec.js │ └── localstorage.spec.js └── configuration │ └── main.spec.js ├── example ├── fetch.html ├── xmlhttprequest.html ├── xmlhttprequest.js └── fetch.js ├── package.json ├── .github └── workflows │ └── codeql-analysis.yml ├── docker-compose.yml ├── dist └── lakka.umd.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | - "node" 5 | -------------------------------------------------------------------------------- /app/adapter/global.js: -------------------------------------------------------------------------------- 1 | require("expose-loader?lakka!./../api/main.js"); 2 | -------------------------------------------------------------------------------- /app/configuration/default.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | "include": [], 3 | "exclude": [], 4 | "minutes": 60 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | ./build/* 2 | ./node_modules/* 3 | ./dev/clientlib-all/js/build/*.js 4 | *lazy*.js 5 | *.min.js 6 | *.bundle.js 7 | ./dev/test/spec/* 8 | _*.js 9 | ./feops/* 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | [ 7 | "@babel/transform-runtime" 8 | ] 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": [ 13 | "mockable-imports", 14 | "istanbul" 15 | ] 16 | }, 17 | "production": { 18 | "presets": [ 19 | "minify" 20 | ] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "---"; 3 | echo "- Build all docker images with the correct context at once" 4 | echo "---"; 5 | 6 | # build the node_modules image with context set to the project root 7 | docker build --no-cache -t lakka-node-modules:latest -f ./docker/node-modules/Dockerfile . 8 | 9 | # build the shell 10 | docker build --no-cache -t lakka-node-shell:latest -f ./docker/node-shell/Dockerfile . 11 | 12 | 13 | -------------------------------------------------------------------------------- /docker/node-shell/Dockerfile: -------------------------------------------------------------------------------- 1 | # use alpine linux with node and yarn and the user "node" 2 | FROM node:16-alpine3.11 3 | 4 | LABEL Description="Front end ops: shell" 5 | 6 | # ENV NPM_CONFIG_LOGLEVEL warn 7 | 8 | # avoid root, switch to the "node" user 9 | USER root 10 | 11 | # set the working directory to the user home 12 | WORKDIR /home/node 13 | 14 | # Install global modules :/ 15 | # RUN yarn global add webpack 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | "root": true, 4 | "extends": "eslint:recommended", 5 | "parserOptions": { "ecmaVersion": 8, "sourceType": "module" }, 6 | "env": { 7 | "browser": true, 8 | "es6": true, 9 | "amd": true, 10 | "node": true 11 | }, 12 | "globals": { 13 | 14 | }, 15 | 16 | "rules": { 17 | "no-console": "off" 18 | } 19 | } 20 | 21 | // sane 22 | // preserve native functionality 23 | // explicit, readable code 24 | // be as verboose as possible 25 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | 2 | import api from './api/main'; 3 | 4 | const time = api["time"]; 5 | const ignore = api["ignore"]; 6 | const recognize = api["recognize"]; 7 | const configuration = api["configuration"]; 8 | const remove = api["remove"]; 9 | const flush = api["flush"]; 10 | const after = api["after"]; 11 | const before = api["before"]; 12 | 13 | export { 14 | time, 15 | ignore, 16 | recognize, 17 | configuration, 18 | remove, 19 | flush, 20 | after, 21 | before 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root=true 5 | 6 | [*] 7 | end_of_line=lf 8 | charset=utf-8 9 | trim_trailing_whitespace=true 10 | insert_final_newline=true 11 | 12 | 13 | # Tabs in CSS, LESS, SASS,HTML 14 | [*.js] 15 | indent_style=tab 16 | [*.html] 17 | indent_style=tab 18 | [*.css] 19 | indent_style=tab 20 | [*.scss] 21 | indent_style=tab 22 | [*.sass] 23 | indent_style=tab 24 | [*.less] 25 | indent_style=tab 26 | 27 | # Spaces in JSON 28 | [*.json] 29 | indent_style=space 30 | # 31 | [*.*rc] 32 | indent_style=space 33 | # 34 | 35 | [*.md] 36 | insert_final_newline=false 37 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | 5 | file: ["./test/common.js"], 6 | timeout: 5000, 7 | recursive: true, 8 | require: '@babel/register', 9 | "spec": ['./test/**/*.spec.js'] 10 | // "spec": [ 11 | // './test/main.spec.js', 12 | // './test/api/main.spec.js', 13 | // './test/api/before.spec.js', 14 | // // './test/api/after.spec.js', 15 | // // './test/utils/*.spec.js', 16 | // // './test/pattern-match/*.spec.js', 17 | // // './test/configuration/*.spec.js', 18 | // // './test/cache/*.spec.js', 19 | // // './test/facade/*.spec.js', 20 | // // './test/header/*.spec.js' 21 | // ] 22 | } 23 | 24 | -------------------------------------------------------------------------------- /app/cache/check-freshness.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module cache/check-freshness 4 | * @exports a sync function 5 | * 6 | * @description 7 | * Checks if a cache item is still fresh 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | 16 | /** 17 | * Checks if a cache item is still fresh 18 | * @sync 19 | * @api 20 | * @param {Object} cacheItem che cache item 21 | * @return {Boolean} true if still fresh 22 | */ 23 | const main = (cacheItem) => !!(cacheItem && cacheItem.until && cacheItem.until >= Date.now()); 24 | export default main; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore ... 2 | # > if you change the .gitignore file, you need to apply the changes with: 3 | # > $ git rm -r --cached . 4 | # > $ git add . 5 | # > $ git commit -m "updated and applied .gitignore" 6 | 7 | 8 | # ### 9 | # project specific, add your patterns here 10 | 11 | coverage/* 12 | coverage.html 13 | node_modules/* 14 | 15 | # ### 16 | ## editor files 17 | .vscode 18 | 19 | # ### 20 | ## osx files 21 | 22 | ._* 23 | .DS* 24 | .Spotlight-V100 25 | .Trashes 26 | 27 | # ### 28 | ## windows files 29 | Thumbs.db 30 | ehthumbs.db 31 | Desktop.ini 32 | $RECYCLE.BIN/ 33 | *.lnk 34 | 35 | # ### 36 | ## misc 37 | .lock 38 | *.log 39 | .vagrant 40 | .git 41 | notes.md 42 | todo.md 43 | specs.md 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/cache/create-key.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module cache/create-key 4 | * @exports a sync function 5 | * 6 | * @description 7 | * Creates the key for the cache from the uri 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | /** 16 | * Creates the key for the lakka-cache from the URI 17 | * @sync 18 | * @memberof cache/create-key 19 | * @param {String} uri the uri 20 | * @return {String} the key for this uri 21 | */ 22 | const main = (uri) => { 23 | if (typeof (uri) !== "string") { 24 | throw new Error(); 25 | } 26 | 27 | return escape(uri); 28 | }; 29 | export default main; 30 | -------------------------------------------------------------------------------- /docker/node-modules/Dockerfile: -------------------------------------------------------------------------------- 1 | # use alpine linux with node and yarn and the user "node" 2 | FROM node:16-alpine3.11 3 | 4 | LABEL Description="All the node modules on a shared volume. If the package.json is changed. Rebuild the image." 5 | 6 | # ENV NPM_CONFIG_LOGLEVEL warn 7 | 8 | # avoid root, switch to the "node" user 9 | # USER node 10 | USER root 11 | 12 | # set the working directory to the user home 13 | WORKDIR /home/node 14 | 15 | # copy files for yarn 16 | COPY ./package.json /home/node/package.json 17 | COPY ./yarn.lock /home/node/yarn.lock 18 | COPY ./.yarnrc /home/node/.yarnrc 19 | 20 | # install from package.json set and remove a cache folder to reduce the image size 21 | RUN set -ex; yarn install --pure-lockfile --production=false --cache-folder ./tmp/.ycache; rm -rf ./tmp/.ycache; 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/utils/throw-invalid.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module utils/throw-invalid 4 | * @exports a sync function 5 | * 6 | * @description 7 | * A small utility function that throws an error if the value evalutes to false 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | /** 16 | * A small utility function that throws an error if the value evalutes to false 17 | * @memberof * @module utils/throw-invalid 18 | * @sync 19 | * @param {Any} value the value to check 20 | * @return {Any|Error} throws an error if the value evalutes to false 21 | */ 22 | const main = (value) => { 23 | if (!value && value !== 0 || value instanceof Error) { 24 | throw new Error(); 25 | } 26 | } 27 | 28 | 29 | export default main; -------------------------------------------------------------------------------- /app/pattern-match/main.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module pattern/match 4 | * @exports a sync function 5 | * 6 | * @description 7 | * checks if the given uri matches one of the patterns 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | /** 16 | * Checks it the uri is matched the regexp-patterns in the array 17 | * @param {String} uri the uri 18 | * @param {Array} patterns an array of regular expressions 19 | * @return {Boolean} true if there's a match 20 | */ 21 | const main = (uri, patterns) => { 22 | if (typeof (uri) !== "string" || Array.isArray(patterns) !== true) { 23 | throw new Error(); 24 | } 25 | 26 | return patterns 27 | .map((regexp) => { 28 | return regexp.test(uri); 29 | }) 30 | .some((match) => { 31 | return match === true; 32 | }); 33 | 34 | }; 35 | export default main; -------------------------------------------------------------------------------- /app/cache/set-item.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module cache/set-item 4 | * @exports a sync function 5 | * 6 | * @description 7 | * A Curried function for setting a specific item from a given cache 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | 16 | import throwIfInvalid from "./../utils/throw-invalid"; 17 | 18 | /** 19 | * Curried function setting the item below "key" at "cache". 20 | * @curried 21 | * @sync 22 | * @api 23 | * @param {Object} cache the cache to use 24 | * @return {Object|Error} the cached item or an Error if we should ignore this uri 25 | */ 26 | const main = (cache) => 27 | /** @param {String} key the key for the lookup @return {function} */ 28 | (key) => 29 | /** @param {Object} item the item to store @return {Object} */ 30 | (item) => { 31 | throwIfInvalid(cache.set(key, item)); 32 | }; 33 | export default main; 34 | -------------------------------------------------------------------------------- /app/cache/get-item.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module cache/get-item 4 | * @exports a sync function 5 | * 6 | * @description 7 | * A Curried function for getting a specific item from a given cache 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | import checkFreshness from "./../cache/check-freshness.js"; 16 | 17 | /** 18 | * Curried function returning the item stored below "key" from "cache". 19 | * @curried 20 | * @sync 21 | * @api 22 | * @param {Object} cache the cache to use 23 | * @return {Object|Error} the cached item or an Error if we should ignore this uri 24 | */ 25 | const main = (cache) => 26 | /** @param {String} key the key for the lookup @return {function} */ 27 | (key) => { 28 | const item = cache.get(key); 29 | // is there a fresh item? 30 | if (!checkFreshness(item)) { 31 | // purge cache 32 | cache.del(key) 33 | // throw; 34 | throw new Error(); 35 | } 36 | return item; 37 | }; 38 | export default main; 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2016 - 2021] Martin Krause 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /app/header/valid-content-type.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module cache/valid-content-type 4 | * @exports a sync function 5 | * 6 | * @description 7 | * Checks if the content-type of the response is valid for LAKKA caching 8 | * 9 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 10 | * @license MIT license: https://opensource.org/licenses/MIT 11 | * 12 | * @author Martin Krause 13 | */ 14 | 15 | const validPattern = [ 16 | new RegExp("application/json"), 17 | new RegExp("text/x-json"), 18 | new RegExp("text/plain"), 19 | new RegExp("text/html") 20 | ]; 21 | 22 | /** 23 | * Checks if the content-type of the response indicates a cachable content 24 | * @sync 25 | * @memberof cache/valid-content-type 26 | * @param {String} element the content of the Content-Type header 27 | * @return {Boolean} true if cachable 28 | */ 29 | const main = (content) => { 30 | if (typeof (content) !== "string" || !content) { 31 | return false; 32 | } 33 | return validPattern.map((regexp) => { 34 | return regexp.test(content); 35 | }) 36 | .some((match) => { 37 | return match === true; 38 | }); 39 | 40 | }; 41 | 42 | export default main; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let plugins = []; 2 | const path = require("path"); 3 | const webpack = require("webpack"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | 6 | plugins.push( 7 | new webpack.BannerPlugin({ 8 | banner: 9 | 'Lakka, github.com/martinkr/Lakka', 10 | }) 11 | ); 12 | 13 | module.exports = { 14 | // mode: 'production', 15 | stats: 'verboose', 16 | entry: { 17 | "umd": "./app/main.js", 18 | }, 19 | devtool: 'source-map', 20 | output: { 21 | clean: true, // Clean the output directory before emit. 22 | library: "lakka", 23 | libraryTarget: "umd", 24 | path: path.resolve(__dirname, "dist"), 25 | filename: "lakka.umd.js" 26 | }, 27 | optimization: { 28 | minimize: true, 29 | minimizer: [ 30 | new TerserPlugin({ 31 | terserOptions: { 32 | format: { 33 | // Say `terser` do not keep license comments 34 | comments: /!/i, 35 | }, 36 | }, 37 | // Say `terser-webpack-plugin` do not create license comments 38 | extractComments: false 39 | }), 40 | ], 41 | }, 42 | plugins: plugins, 43 | module: { 44 | rules: [ 45 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } 46 | ] 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | global.chai = require("chai"); 4 | global.chai.should(); 5 | 6 | global.expect = global.chai.expect; 7 | 8 | global.sinon = require("sinon"); 9 | global.sinonChai = require("sinon-chai"); 10 | global.chai.use(global.sinonChai); 11 | 12 | // global.chaiAsPromised = require("chai-as-promised"); 13 | // global.chai.use(global.chaiAsPromised); 14 | 15 | global.ENV = "test"; 16 | 17 | if (!global.window) { 18 | global.window = {}; 19 | } 20 | 21 | // mock localStorage 22 | if (!global.window.localStorage) { 23 | 24 | global.window.localStorage = { 25 | _data: {}, 26 | length() { return Object.keys(global.window.localStorage._data).length }, 27 | inspect() { 28 | // todo 29 | // split items into lines 30 | }, 31 | getItem(key) { 32 | if (global.window.localStorage._data.hasOwnProperty(key) === false) { 33 | return null; 34 | } 35 | return global.window.localStorage._data[key]; 36 | }, 37 | setItem(key, value) { 38 | return global.window.localStorage._data[key] = value; 39 | }, 40 | removeItem(key) { 41 | delete global.window.localStorage._data[key]; 42 | }, 43 | clear() { 44 | global.window.localStorage._data = {}; 45 | }, 46 | }; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /test/cache/create-key.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for creating the key out of the uri 3 | * 4 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 5 | * @license MIT license: https://opensource.org/licenses/MIT 6 | * 7 | * @author Martin Krause 8 | */ 9 | 10 | /* eslint-env mocha */ 11 | 12 | import thisModule from "./../../app/cache/create-key"; 13 | const thisModulePath = "cache/create-key"; 14 | 15 | const uri = "protocol://path/to/my/resouce"; 16 | const validKey = escape(uri); 17 | 18 | describe(`The module "${thisModulePath}"`, () => { 19 | 20 | afterEach((done) => { 21 | done(); 22 | }); 23 | 24 | beforeEach((done) => { 25 | done(); 26 | }); 27 | 28 | describe("should provide an unified API. It:", () => { 29 | 30 | it("should export a sync function ", () => { 31 | thisModule.should.be.a("function"); 32 | }); 33 | 34 | it("should throw an error if the first argument is not a string", () => { 35 | try { 36 | thisModule(true); 37 | } catch (e) { 38 | e.should.be.an("error"); 39 | return true; 40 | } 41 | (true).should.not.be.true; 42 | }); 43 | 44 | it("should return the key", () => { 45 | thisModule(uri).should.equal(validKey); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/sanity.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import * as fs from "fs-extra-plus"; 4 | 5 | const timeout = (fn) => new Promise((resolve) => setTimeout(() => resolve(fn()), 250)); 6 | 7 | 8 | describe("sanity checks for the mocha test suite", () => { 9 | describe("the mocha async wrapper should work as expected", () => { 10 | 11 | it("Simple async/await mocha test", (async () => { 12 | let x = await timeout(() => "Hello World!"); 13 | (x).should.equal("Hello World!"); 14 | })); 15 | 16 | it("the mocha async wrapper should work as expected with mutliple async / awaits", (async () => { 17 | let x = await timeout(() => "Hello World!x"); 18 | let y = await timeout(() => "Hello World!y"); 19 | let z = await timeout(() => "Hello World!z"); 20 | (x).should.equal("Hello World!x"); 21 | (y).should.equal("Hello World!y"); 22 | (z).should.equal("Hello World!z"); 23 | })); 24 | 25 | it("the mocha async wrapper should work with the filesystem", (async () => { 26 | let _fileTrue = "./test/sanity.spec.js"; 27 | let _fileFalse = "./test/none.txt"; 28 | let x = await timeout(() => fs.pathExists(_fileTrue)); 29 | let y = await timeout(() => fs.pathExists(_fileFalse)); 30 | (x).should.be.ok; 31 | (y).should.not.be.ok; 32 | })); 33 | 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /app/header/utils/get-value.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module header/utils/get-value 3 | * @exports a sync function 4 | * 5 | * @description 6 | * Curried function returning the value or "defaultValue" 7 | * for the header-property "which" from the "headers" object. 8 | * Handles the the default value fallback and lowercase properties 9 | * 10 | * 11 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 12 | * @license MIT license: https://opensource.org/licenses/MIT 13 | * 14 | * @author Martin Krause 15 | */ 16 | 17 | /** 18 | * Curried function returning the value or "defaultValue" 19 | * for the header-property "which" from the "headers" object. 20 | * Handles the the default value fallback and lowercase properties 21 | * @curried 22 | * @sync 23 | * @memberof cache/create-item 24 | * @param {Object} headers the headers object 25 | * @returns {String|null} 26 | */ 27 | const main = (headers) => 28 | /** @param {String} which the name of the header to look for @return {function} */ 29 | (which) => 30 | /** @param {Any} [defaultValue] the optional default value @return {function} */ 31 | (defaultValue) => { 32 | if (!headers && defaultValue) { return defaultValue; } 33 | if (!headers && !defaultValue || !which) { return null; } 34 | let value = headers[which] || headers[which.toLowerCase()]; 35 | if (value) { return value; } 36 | if (!value && defaultValue) { return defaultValue; } 37 | return null; 38 | }; 39 | 40 | 41 | export default main; 42 | -------------------------------------------------------------------------------- /app/header/utils/check-value.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module header/utils/checl-value 3 | * @exports a sync function 4 | * 5 | * @description 6 | * Curried function returning if the value for a "header" property 7 | * inside the "options.headers" object is valid as decided by "fn" 8 | * 9 | * 10 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 11 | * @license MIT license: https://opensource.org/licenses/MIT 12 | * 13 | * @author Martin Krause 14 | */ 15 | 16 | import throwIfInvalid from "./../../utils/throw-invalid"; 17 | 18 | /** 19 | * Curried function returning if the value for a "header" property 20 | * inside the "options.headers" object is valid as decided by "fn" 21 | * @curried 22 | * @sync 23 | * @param {Function} fn which function to use for validating 24 | * @return {Boolean|Error} throws an Error if we should ignore this uri, defaults to true even if there's no value 25 | */ 26 | const main = (fn) => 27 | /** @param {Object} options the options object, contains a headers object @return {function} */ 28 | (options) => 29 | /** @param {String} header the header to look up @return {function} */ 30 | (header) => { 31 | if (!fn || typeof (fn) !== "function") { throw new Error(); } 32 | if (!options || !options.headers || !header) { return true; } 33 | let value = options.headers[header] || options.headers[header.toLowerCase()]; 34 | if (!value) { return true; } 35 | throwIfInvalid(fn(value)); 36 | return true; 37 | }; 38 | 39 | export default main; 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/main.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for setting a specific item from a given cache 3 | * 4 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 5 | * @license MIT license: https://opensource.org/licenses/MIT 6 | * 7 | * @author Martin Krause 8 | */ 9 | 10 | /* eslint-env mocha */ 11 | 12 | import * as thisModule from "./../app/main"; 13 | 14 | const thisModulePath = "main"; 15 | 16 | describe(`The module "${thisModulePath}"`, () => { 17 | 18 | 19 | 20 | before((done) => { 21 | done(); 22 | }); 23 | 24 | 25 | 26 | after((done) => { 27 | done(); 28 | }); 29 | 30 | describe("should provide an unified API. It:", () => { 31 | 32 | it("should export a sync function \"configuration\"", () => { 33 | thisModule.configuration.should.be.a("function"); 34 | }); 35 | 36 | it("should export a sync function \"time\"", () => { 37 | thisModule.time.should.be.a("function"); 38 | }); 39 | 40 | it("should export a sync function \"recognize\"", () => { 41 | thisModule.recognize.should.be.a("function"); 42 | }); 43 | 44 | it("should export a sync function \"ignore\"", () => { 45 | thisModule.ignore.should.be.a("function"); 46 | }); 47 | 48 | it("should export a sync function \"flush\"", () => { 49 | thisModule.flush.should.be.a("function"); 50 | }); 51 | 52 | it("should export a sync function \"after\"", () => { 53 | thisModule.after.should.be.a("function"); 54 | }); 55 | 56 | it("should export a sync function \"before\"", () => { 57 | thisModule.before.should.be.a("function"); 58 | }); 59 | 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/header/valid-status-code.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for determining if the content is cachable by lakka. 3 | * Lakka agressively caches json, html and text responses 4 | * We're looking at the content-type to find 5 | * 6 | * json 7 | * - "application/json", 8 | * - "text/x-json", 9 | * 10 | * text 11 | * - "text/plain" 12 | * 13 | * html 14 | * - "text/html" 15 | * 16 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 17 | * @license MIT license: https://opensource.org/licenses/MIT 18 | * 19 | * @author Martin Krause 20 | */ 21 | 22 | /* eslint-env mocha */ 23 | 24 | import thisModule from "./../../app/header/valid-status-code"; 25 | const thisModulePath = "header/valid-status-code"; 26 | 27 | // these are the ones being detested 28 | const validTyes = [ 29 | "200", 30 | "203", 31 | "226" 32 | ]; 33 | 34 | // this is just an arbitrary list 35 | const invalidTyes = [ 36 | "100", 37 | "300", 38 | "400" 39 | ]; 40 | 41 | 42 | 43 | describe(`The module "${thisModulePath}"`, () => { 44 | 45 | afterEach((done) => { 46 | done(); 47 | }); 48 | 49 | beforeEach((done) => { 50 | done(); 51 | }); 52 | 53 | describe("should provide an unified API. It:", () => { 54 | 55 | it("should export a sync function ", () => { 56 | thisModule.should.be.a("function"); 57 | }); 58 | 59 | it("should return false if there's no argument", () => { 60 | thisModule().should.be.false; 61 | }); 62 | 63 | invalidTyes.forEach((obj) => { 64 | it(`should return false if the Content-Type is "${obj}"`, () => { 65 | thisModule(obj).should.be.false; 66 | }); 67 | }); 68 | 69 | validTyes.forEach((obj) => { 70 | it(`should return true if the Content-Type is "${obj}"`, () => { 71 | thisModule(obj).should.be.true; 72 | }); 73 | }); 74 | 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/cache/set-item.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for setting a specific item from a given cache 3 | * 4 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 5 | * @license MIT license: https://opensource.org/licenses/MIT 6 | * 7 | * @author Martin Krause 8 | */ 9 | 10 | /* eslint-env mocha */ 11 | 12 | import thisModule from "./../../app/cache/set-item"; 13 | const thisModulePath = "cache/set-item"; 14 | 15 | // mock the cache object 16 | // spy on the call, check parameters passed to cache 17 | 18 | let facade; 19 | let spyFacade; 20 | 21 | describe(`The module "${thisModulePath}"`, () => { 22 | 23 | before(() => { 24 | 25 | facade = { _data: {}, "set": (key, value) => facade._data[key] = value }; 26 | spyFacade = sinon.spy(facade, "set"); 27 | 28 | }); 29 | 30 | beforeEach(() => { 31 | facade._data = {}; 32 | }) 33 | 34 | after(() => { 35 | spyFacade.restore(); 36 | global.window.localStorage.clear(); 37 | }) 38 | afterEach(() => { 39 | spyFacade.resetHistory(); 40 | }); 41 | 42 | 43 | describe("should provide an unified API. It:", () => { 44 | 45 | it("should export a sync function ", () => { 46 | thisModule.should.be.a("function"); 47 | }); 48 | 49 | it("should call the underlying cache with the correct parameters", () => { 50 | try { 51 | thisModule(facade)("key")("value"); 52 | spyFacade.should.have.been.called.with(facade, "key", "value"); 53 | } catch (err) { 54 | return true; 55 | } 56 | return true; 57 | }); 58 | 59 | 60 | it("should throw if there's an error", () => { 61 | delete facade._data; 62 | try { 63 | thisModule(facade)("key")("value");; 64 | } catch (err) { 65 | err.should.be.an("error"); 66 | return true; 67 | } 68 | throw new Error("Failed"); 69 | }); 70 | 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /example/fetch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | blank 8 | 9 | 10 | 57 | 58 | 59 | 60 | 61 |

fetch example

62 |

nothing to see here, check the console

63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /example/xmlhttprequest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | blank 8 | 9 | 10 | 57 | 58 | 59 | 60 | 61 |

fetch example

62 |

nothing to see here, check the console

63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /test/header/valid-content-type.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for determining if the content is cachable by lakka. 3 | * Lakka agressively caches json, html and text responses 4 | * We're looking at the content-type to find 5 | * 6 | * json 7 | * - "application/json", 8 | * - "text/x-json", 9 | * 10 | * text 11 | * - "text/plain" 12 | * 13 | * html 14 | * - "text/html" 15 | * 16 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 17 | * @license MIT license: https://opensource.org/licenses/MIT 18 | * 19 | * @author Martin Krause 20 | */ 21 | 22 | /* eslint-env mocha */ 23 | 24 | import thisModule from "./../../app/header/valid-content-type"; 25 | const thisModulePath = "header/valid-content-type"; 26 | 27 | // these are the ones being detested 28 | const validTyes = [ 29 | "application/json", 30 | "text/x-json", 31 | "text/plain", 32 | "charset=utf-8; text/plain", 33 | "text/html", 34 | "text/html; charset=utf-8" 35 | ]; 36 | 37 | // this is just an arbitrary list 38 | const invalidTyes = [ 39 | "text/css", 40 | "text/xml", 41 | "image/gif", 42 | "image/jpeg", 43 | "application/x-javascript", 44 | "text/x-component", 45 | "text/mathml", 46 | "image/png", 47 | "image/x-icon", 48 | "application/zip", 49 | "audio/mpeg", 50 | "video/mpeg" 51 | ]; 52 | 53 | 54 | 55 | describe(`The module "${thisModulePath}"`, () => { 56 | 57 | afterEach((done) => { 58 | done(); 59 | }); 60 | 61 | beforeEach((done) => { 62 | done(); 63 | }); 64 | 65 | describe("should provide an unified API. It:", () => { 66 | 67 | it("should export a sync function ", () => { 68 | thisModule.should.be.a("function"); 69 | }); 70 | 71 | it("should return false if there's no argument", () => { 72 | thisModule().should.be.false; 73 | }); 74 | 75 | invalidTyes.forEach((obj) => { 76 | it(`should return false if the Content-Type is "${obj}"`, () => { 77 | thisModule(obj).should.be.false; 78 | }); 79 | }); 80 | 81 | validTyes.forEach((obj) => { 82 | it(`should return true if the Content-Type is "${obj}"`, () => { 83 | thisModule(obj).should.be.true; 84 | }); 85 | }); 86 | 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /app/header/valid-cache-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module cache/valid-cache-control 3 | * @exports a sync function 4 | * 5 | * @description 6 | * Checks the value of the "Cache-Control" HTTP-Header. 7 | * Checks if the Cache-Control- HTTP-Header let's us cache this request. 8 | * 9 | * Agressive cache: 10 | * Do not cache if 11 | * must-revalidate 12 | * The cache must verify the status of the stale resources before using it and expired ones should not be used. 13 | * no-store 14 | * The cache should not store anything about the client request or server response. 15 | * no-cache 16 | * Forces caches to submit the request to the origin server for validation before releasing a cached copy. 17 | * 18 | * OBSOLETE 19 | * Cache if the value is: 20 | * only-if-cached (Request) 21 | * Indicates to not retrieve new data. The client only wishes to obtain a cached response, and should not contact the origin-server to see if a newer copy exists. 22 | * immutable 23 | * Indicates that the response body will not change over time. 24 | * public 25 | * Indicates that the response may be cached by any cache. 26 | * private 27 | * Indicates that the response is intended for a single user and must not be stored by a shared cache. A private cache may store the response. 28 | * 29 | * or no entry at all 30 | * 31 | * 32 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 33 | * @license MIT license: https://opensource.org/licenses/MIT 34 | * 35 | * @author Martin Krause 36 | */ 37 | 38 | const ignorePattern = [ 39 | new RegExp("must-revalidate"), 40 | new RegExp("no-store"), 41 | new RegExp("no-cache") 42 | ]; 43 | 44 | /** 45 | * Takes the content of the "Cache-Control" HTTP-Header 46 | * as string and checks if we can cache the request. 47 | * @sync 48 | * @memberof cache/valid-cache-control 49 | * @param {String} the "Cache-Control" HTTP-Header's content 50 | * @return {Boolean} true if it is cacheable, false if not 51 | */ 52 | const main = (content) => { 53 | if (typeof (content) !== "string" || !content) { 54 | return true; 55 | } 56 | return ignorePattern.map((regexp) => { 57 | return regexp.test(content); 58 | }).every((match) => { 59 | return match === false; 60 | }); 61 | 62 | }; 63 | 64 | export default main; -------------------------------------------------------------------------------- /test/header/valid-cache-control.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for checking if the "Cache-Control" HTTP-Header content prevents us from caching the request 3 | * (inbound and outbound) 4 | * 5 | * LAKKA is agressive. We're caching everything except if the header contains: 6 | * "must-revalidate" 7 | * The cache must verify the status of the stale resources before using it and expired ones should not be used. 8 | * "no-store" 9 | * The cache should not store anything about the client request or server response. 10 | * "no-cache" 11 | * Forces caches to submit the request to the origin server for validation before releasing a cached copy. 12 | * 13 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 14 | * @license MIT license: https://opensource.org/licenses/MIT 15 | * 16 | * @author Martin Krause 17 | */ 18 | 19 | /* eslint-env mocha */ 20 | 21 | 22 | import thisModule from "./../../app/header/valid-cache-control"; 23 | const thisModulePath = "header/valid-cache-control"; 24 | 25 | const headersIgnore = [{ 26 | "pattern": "must-revalidate", 27 | "content": "must-revalidate" 28 | }, 29 | { 30 | "pattern": "no-store", 31 | "content": "no-store" 32 | }, 33 | { 34 | "pattern": "no-cache", 35 | "content": "no-store" 36 | }, 37 | ]; 38 | 39 | 40 | 41 | describe(`The module "${thisModulePath}"`, () => { 42 | 43 | afterEach((done) => { 44 | done(); 45 | }); 46 | 47 | beforeEach((done) => { 48 | done(); 49 | }); 50 | 51 | describe("should provide an unified API. It:", () => { 52 | 53 | it("should export a sync function ", () => { 54 | thisModule.should.be.a("function"); 55 | }); 56 | 57 | it("should return true if there's no argument", () => { 58 | thisModule().should.be.true; 59 | }); 60 | 61 | it("should return true if the first argument is not a String", () => { 62 | thisModule([]).should.be.true; 63 | }); 64 | 65 | it(`should return true if the string contains does not contain an ignore pattern`, () => { 66 | thisModule("immutable, max-age=500, s-maxage=500").should.be.true; 67 | }); 68 | 69 | headersIgnore.forEach((obj) => { 70 | it(`should return false if the string contains "${obj.pattern}"`, () => { 71 | thisModule(obj.content).should.be.false; 72 | }); 73 | }); 74 | 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/cache/check-freshness.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for the module checking for freshness 3 | * 4 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 5 | * @license MIT license: https://opensource.org/licenses/MIT 6 | * 7 | * @author Martin Krause 8 | */ 9 | 10 | /* eslint-env mocha */ 11 | 12 | import thisModule from "./../../app/cache/check-freshness"; 13 | const thisModulePath = "cache/check-freshness"; 14 | 15 | describe(`The module "${thisModulePath}"`, () => { 16 | 17 | 18 | describe("should provide an unified API. It:", () => { 19 | 20 | it("should be a sync function", () => { 21 | (Object.getPrototypeOf(thisModule).constructor.name === "Function").should.be.ok; 22 | }); 23 | }); 24 | 25 | describe("should consume one argument. It:", () => { 26 | 27 | it("should return false if there are no arguments", (() => { 28 | thisModule().should.be.false; 29 | })); 30 | 31 | it("should return false if the first argument is a \"String\" and not an \"Object\"", (() => { 32 | thisModule("string").should.be.false; 33 | })); 34 | 35 | it("should return false if the first argument is an \"Array\" and not an \"Object\"", (() => { 36 | thisModule([]).should.be.false; 37 | })); 38 | 39 | it("should return false if the first argument is a \"Number\" and not an \"Object\"", (() => { 40 | thisModule(1).should.be.false; 41 | })); 42 | 43 | it("should return false if the first argument is a boolean \"false\" and not an \"Object\"", (() => { 44 | thisModule(false).should.be.false; 45 | })); 46 | 47 | it("should return false if the first argument is a boolean \"true\" and not an \"Object\"", (() => { 48 | thisModule(true).should.be.false; 49 | })); 50 | 51 | it("should return false if the first argument is an \"Object\" without \".until\"", (() => { 52 | thisModule({ "foo": "bar" }).should.be.false; 53 | })); 54 | 55 | 56 | }); 57 | 58 | describe("should work as expected. It:", () => { 59 | 60 | it("should return false if there item is stale", (() => { 61 | thisModule({ "until": Date.now() - 10000 }).should.be.false; 62 | })); 63 | 64 | it("should return true if there item is fresh", (() => { 65 | thisModule({ "until": Date.now() + 10000 }).should.be.true; 66 | })); 67 | 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lakka", 3 | "version": "3.0.0", 4 | "description": "lakka. An asynchronous request accelerator.", 5 | "main": "./app/main.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/martinkr/lakka.git" 10 | }, 11 | "homepage": "https://github.com/martinkr/lakka", 12 | "issues": "https://github.com/martinkr/lakka/issues", 13 | "author": "Martin Krause (http://martinkr.github.io)", 14 | "keywords": [], 15 | "scripts": { 16 | "prebuild": "yarn test", 17 | "build": "yarn webpack ", 18 | "eslint": "npx eslint ./app/**/*.js", 19 | "mocha": "NODE_ENV=test npx mocha --reporter spec", 20 | "nyc": "NODE_ENV=test npx nyc --clean npx mocha", 21 | "coverage": "npx nyc check-coverage --lines 100 --functions 100 --branches 100 --statements 100", 22 | "report": "NODE_ENV=test npx nyc report --reporter=lcov --reporter=html", 23 | "test": "yarn eslint && yarn nyc && yarn report && yarn coverage", 24 | "mocha-watch": "NODE_ENV=test npx mocha --reporter min -w", 25 | "eslint-watch": "npx esw --watch ./app/**/*.js", 26 | "webpack": "npx webpack --mode production --env production" 27 | }, 28 | "nyc": { 29 | "reporter": [ 30 | "lcov", 31 | "text", 32 | "html" 33 | ], 34 | "sourceMap": false, 35 | "instrument": false 36 | }, 37 | "dependencies": { 38 | "@babel/runtime": "^7.15.4", 39 | "esm": "^3.2.25" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.0.0", 43 | "@babel/plugin-transform-runtime": "^7.15.8", 44 | "@babel/preset-env": "^7.0.0", 45 | "@babel/register": "^7.15.3", 46 | "babel-loader": "^8.2.2", 47 | "babel-plugin-istanbul": "^6.0.0", 48 | "babel-plugin-mockable-imports": "^1.8.0", 49 | "babel-preset-minify": "^0.5.1", 50 | "chai": "^4.3.4", 51 | "eslint": "^7.32.0", 52 | "eslint-watch": "7.0.0", 53 | "expose-loader": "^3.0.0", 54 | "fs-extra-plus": "0.5.22", 55 | "mocha": "^9.1.2", 56 | "nyc": "^15.1.0", 57 | "sinon": "^11.1.2", 58 | "sinon-chai": "^3.7.0", 59 | "terser-webpack-plugin": "^5.2.4", 60 | "webpack": "^5.58.0", 61 | "webpack-cli": "^4.9.0" 62 | }, 63 | "bugs": { 64 | "url": "https://github.com/martinkr/lakka/issues" 65 | }, 66 | "directories": { 67 | "test": "test" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/pattern-match/match.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for the pattern match function 3 | * This module provides the specs for the 4 | * pattern match functionality 5 | * 6 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 7 | * @license MIT license: https://opensource.org/licenses/MIT 8 | * 9 | * @author Martin Krause 10 | */ 11 | 12 | /* eslint-env mocha */ 13 | 14 | import thisModule from "./../../app/pattern-match/main"; 15 | const thisModulePath = "pattern-match/main"; 16 | 17 | const uri = "protocol://path/to/my/resouce"; 18 | let patterns; 19 | 20 | describe(`The module "${thisModulePath}"`, () => { 21 | 22 | afterEach((done) => { 23 | done(); 24 | }); 25 | 26 | beforeEach((done) => { 27 | patterns = []; 28 | done(); 29 | }); 30 | 31 | describe("should provide an unified API. It:", () => { 32 | 33 | it("should export a sync function ", () => { 34 | thisModule.should.be.a("function"); 35 | }); 36 | 37 | it("should throw an error if the first argument is not a string", () => { 38 | try { 39 | thisModule(true, patterns); 40 | } catch (e) { 41 | e.should.be.an("error"); 42 | return true; 43 | } 44 | (true).should.not.be.true; 45 | }); 46 | 47 | it("should throw an error if the second argument is not an array", () => { 48 | try { 49 | thisModule(uri, {}); 50 | } catch (e) { 51 | e.should.be.an("error"); 52 | return true; 53 | } 54 | (true).should.not.be.true; 55 | }); 56 | 57 | it("should throw an error if one argument is missing", () => { 58 | try { 59 | thisModule("foo"); 60 | } catch (e) { 61 | e.should.be.an("error"); 62 | return true; 63 | } 64 | (true).should.not.be.true; 65 | }); 66 | 67 | it("should throw an error if both arguments are missing", () => { 68 | try { 69 | thisModule(); 70 | } catch (e) { 71 | e.should.be.an("error"); 72 | return true; 73 | } 74 | (true).should.not.be.true; 75 | }); 76 | 77 | it("should return true if the regular expression does match the uri", (async () => { 78 | patterns.push(new RegExp("path")); 79 | thisModule(uri, patterns).should.be.true; 80 | })); 81 | 82 | it("should return false if the regular expression does not match the uri", (async () => { 83 | patterns.push(new RegExp("fail")); 84 | thisModule(uri, patterns).should.be.false; 85 | })); 86 | 87 | 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /app/header/valid-status-code.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module header/valid-status-code 4 | * @exports a sync function 5 | * 6 | * @description 7 | * Checks if the status-code of the response indicates cachable content 8 | * 9 | * 200 OK 10 | * Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request, the response will contain an entity describing or containing the result of the action.[8] 11 | * 203 Non-Authoritative Information (since HTTP/1.1) 12 | * The server is a transforming proxy (e.g. a Web accelerator) that received a 200 OK from its origin, but is returning a modified version of the origin's response.[11][12] 13 | * 226 IM Used (RFC 3229) 14 | * The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.[17] 15 | * 16 | * TODO think about these 17 | * 301 Moved Permanently 18 | * This and all future requests should be directed to the given URI.[20] 19 | * 302 Found 20 | * 303 See Other (since HTTP/1.1) 21 | * The response to the request can be found under another URI using the GET method. 22 | * 304 Not Modified (RFC 7232) 23 | * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy.[25] 24 | * 308 Permanent Redirect (RFC 7538) 25 | * The request and all future requests should be repeated using another URI. 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. So, for example, submitting a form to a permanently redirected resource may continue smoothly.[30] 26 | * 27 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 28 | * @license MIT license: https://opensource.org/licenses/MIT 29 | * 30 | * @author Martin Krause 31 | */ 32 | 33 | /** 34 | * Checks if the status-code of the response indicates a cachable content 35 | * @memberof header/valid-status-code 36 | * @sync 37 | * @param {String} element the content of the Status-Code of the response 38 | * @return {Boolean} true if cachable 39 | */ 40 | const main = (element) => ["200", "203", "226"].includes(element); 41 | 42 | export default main; -------------------------------------------------------------------------------- /app/cache/create-item.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module cache/create-item 4 | * @exports a sync function 5 | * 6 | * @description 7 | * 8 | * Creates a cache-item with 9 | * 10 | * "key": create-key(uri) 11 | * "until": freshness([default, "cache-control", "expires"]) 12 | * "headers" : 13 | * "X-Status-Code": 200 14 | * "Cache-Control": 15 | * "Expires": 16 | * "Content-Type": application-type 17 | * "Status": "200 from cache" 18 | * "responseText": string 19 | * 20 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 21 | * @license MIT license: https://opensource.org/licenses/MIT 22 | * 23 | * @author Martin Krause 24 | */ 25 | import configuration from "./../configuration/main"; 26 | import createKey from "./../cache/create-key"; 27 | import calculateValidity from "./../cache/calculate-validity.js"; 28 | import getHeaderValue from "./../header/utils/get-value.js"; 29 | 30 | const defaultMinutes = configuration.get("minutes"); 31 | const defaultMiliseconds = defaultMinutes * 60000; 32 | 33 | 34 | /** 35 | * Creates the item for the lakka-cache from the URI, responseString and the headers opbject 36 | * @sync 37 | * @memberof cache/create-item 38 | * @param {String} uri the uri 39 | * @param {String} responseString the response 40 | * @param {Object} [headers] the optional header object 41 | * @return {Object} a cache item 42 | */ 43 | const main = (uri, responseString, headers) => { 44 | // we're only accepting strings as the first and second and an optional object as third parameter 45 | if (typeof (uri) !== "string" || typeof (responseString) !== "string" || (headers && headers instanceof Object && headers.constructor === Object) === false) { 46 | throw new Error(); 47 | } 48 | let _item = { "headers": {} }; 49 | try { 50 | _item.key = createKey(uri); 51 | _item.status = 200; 52 | _item.statusText = "cache" 53 | _item.responseText = responseString; 54 | _item.until = calculateValidity(getHeaderValue(headers)("Cache-Control")(null), getHeaderValue(headers)("Expires")(null), defaultMiliseconds); 55 | _item.headers["Status"] = `${_item.status} ${_item.statusText}`; 56 | _item.headers["Content-Type"] = getHeaderValue(headers)("Content-Type")("text/plain"); 57 | _item.headers["Cache-Control"] = getHeaderValue(headers)("Cache-Control")(null); 58 | _item.headers["Expires"] = getHeaderValue(headers)("Expires")(null); 59 | // purge 60 | Object.keys(_item.headers).forEach((key) => (_item.headers[key] == null) && delete _item.headers[key]); 61 | } catch (err) { 62 | // safety net ... 63 | /* istanbul ignore next */ 64 | throw new Error(); 65 | } 66 | return _item; 67 | }; 68 | 69 | export default main; 70 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '31 23 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | 4 | # assumption: this just shares the node modules and exits 5 | node-modules: 6 | image: lakka-node-modules:latest 7 | volumes: 8 | # create a named volume "node_modules" 9 | - "shared-node-modules:/home/node/node_modules/" 10 | command: 11 | "tail -f /dev/null" 12 | 13 | # shell 14 | node-shell: 15 | image: lakka-node-shell:latest 16 | volumes: 17 | # get node_modules from shared volume 18 | - "shared-node-modules:/home/node/node_modules/" 19 | - "./app:/home/node/app" 20 | - "./dist:/home/node/dist" 21 | - "./test:/home/node/test" 22 | - "./coverage:/home/node/coverage" 23 | - "./package.json:/home/node/package.json" 24 | - "./.eslintrc.js:/home/node/.eslintrc.js:ro" 25 | - "./.eslintignore:/home/node/.eslintignore:ro" 26 | - "./.babelrc:/home/node/.babelrc" 27 | - "./.mocharc.js:/home/node/.mocharc.js:ro" 28 | - "./webpack.config.js:/home/node/webpack.config.js" 29 | - "./yarn.lock:/home/node/yarn.lock" 30 | depends_on: 31 | - node-modules 32 | command: 33 | "tail -f /dev/null" 34 | 35 | # container for eslint watcher 36 | # $ docker-compose logs -f -t node-eslint-watch 37 | node-eslint-watch: 38 | image: lakka-node-shell:latest 39 | volumes: 40 | # get node_modules from shared volume 41 | - "shared-node-modules:/home/node/node_modules/:ro" 42 | - "./app:/home/node/app:ro" 43 | - "./.eslintrc.js:/home/node/.eslintrc.js:ro" 44 | - "./.eslintignore:/home/node/.eslintignore:ro" 45 | - "./package.json:/home/node/package.json:ro" 46 | depends_on: 47 | - node-modules 48 | - node-shell 49 | command: 50 | "yarn run eslint-watch" 51 | 52 | # container for mocha watcher 53 | # $ docker-compose logs -f -t node-mocha-watch 54 | node-mocha-watch: 55 | image: lakka-node-shell:latest 56 | volumes: 57 | # get node_modules from shared volume 58 | - "shared-node-modules:/home/node/node_modules/:ro" 59 | - "./app:/home/node/app:ro" 60 | - "./test:/home/node/test:ro" 61 | - "./package.json:/home/node/package.json:ro" 62 | - "./.babelrc:/home/node/.babelrc:ro" 63 | - "./.mocharc.js:/home/node/.mocharc.js:ro" 64 | depends_on: 65 | - node-modules 66 | - node-shell 67 | command: 68 | "yarn run mocha-watch" 69 | 70 | 71 | volumes: 72 | shared-node-modules: 73 | -------------------------------------------------------------------------------- /test/cache/get-item.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for getting a specific item from a given cache 3 | * 4 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 5 | * @license MIT license: https://opensource.org/licenses/MIT 6 | * 7 | * @author Martin Krause 8 | */ 9 | 10 | /* eslint-env mocha */ 11 | 12 | import thisModule from "./../../app/cache/get-item"; 13 | const thisModulePath = "cache/get-item"; 14 | 15 | // mock the cache object 16 | // spy on the call, check parameters passed to cache 17 | 18 | let facade; 19 | let spyFacade; 20 | 21 | describe(`The module "${thisModulePath}"`, () => { 22 | 23 | before(() => { 24 | 25 | facade = { _data: {}, "get": (key) => facade._data[key] }; 26 | spyFacade = sinon.spy(facade, "get"); 27 | 28 | }); 29 | 30 | beforeEach(() => { 31 | facade._data = {}; 32 | }) 33 | 34 | after(() => { 35 | spyFacade.restore(); 36 | global.window.localStorage.clear(); 37 | }) 38 | afterEach(() => { 39 | spyFacade.resetHistory(); 40 | }); 41 | 42 | 43 | 44 | describe("should provide an unified API. It:", () => { 45 | 46 | it("should export a sync function ", () => { 47 | thisModule.should.be.a("function"); 48 | }); 49 | 50 | it("should call the underlying cache with the correct parameters", () => { 51 | try { 52 | thisModule(facade)("key"); 53 | spyFacade.should.have.been.called.with(facade, "key"); 54 | } catch (err) { 55 | return true; 56 | } 57 | return true; 58 | }); 59 | 60 | it("should return a fresh item", () => { 61 | facade._data["key"] = { 62 | "foo": "fresh", 63 | "until": Date.now() + 1000 64 | }; 65 | thisModule(facade)("key").foo.should.equal("fresh"); 66 | }); 67 | 68 | it("should throw if there's no item for this key", () => { 69 | delete facade._data["key"]; 70 | 71 | try { 72 | thisModule(facade)("key"); 73 | } catch (err) { 74 | err.should.be.an("error"); 75 | return true; 76 | } 77 | throw new Error("Failed"); 78 | }); 79 | 80 | it("should throw if there's a stale item for this key", () => { 81 | facade._data["key"] = { 82 | "foo": "fresh", 83 | "until": Date.now() - 1000 84 | }; 85 | try { 86 | thisModule(facade)("key"); 87 | } catch (err) { 88 | err.should.be.an("error"); 89 | return true; 90 | } 91 | throw new Error("Failed"); 92 | }); 93 | 94 | it("should remote the item from the cache if the item is stale", () => { 95 | facade._data["key"] = { 96 | "foo": "fresh", 97 | "until": Date.now() - 1000 98 | }; 99 | try { 100 | thisModule(facade)("key"); 101 | } catch (err) { 102 | (global.window.localStorage._data["key"] === undefined).should.be.true; 103 | return true; 104 | } 105 | throw new Error("Failed"); 106 | }); 107 | 108 | }); 109 | 110 | }); 111 | 112 | 113 | -------------------------------------------------------------------------------- /example/xmlhttprequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function makes the XHR-call 3 | * if there's no item in the lakka-cache and 4 | * stores the received data afterwards. 5 | * It's a basic example how to integrate lakka 6 | * into your scripts using the XMLHttpRequest-Object. 7 | * Assumes you already included "../dist/lakka.js" 8 | * so window.lakka is available. 9 | * @param {String} The URI to call 10 | * @param {Function} A traditional oncomplete-callback 11 | */ 12 | var lakkaXHR = function (uri, callback) { 13 | var _httpRequest; 14 | var _response; 15 | // get the cached content from lakka 16 | try { 17 | // yay! content from the cache 18 | _response = window.lakka.before(uri, {}); 19 | // pass it to the callback 20 | return callback(_response); 21 | } catch (err) { 22 | // lakka throws an error if it can not return a fresh cache item 23 | // don't worry, just proceed to the request 24 | } 25 | 26 | // create the XHR-object 27 | try { 28 | _httpRequest = new XMLHttpRequest(); 29 | } catch (err) { 30 | console.error("Error: failed on creating the XMLHttpRequest-object :(. "); 31 | return false; 32 | } 33 | 34 | // set up the request 35 | _httpRequest.onreadystatechange = function () { 36 | var _statusCode, _responseText, _options, _cacheItem; 37 | 38 | // look, when the request is done 39 | if (_httpRequest.readyState === XMLHttpRequest.DONE) { 40 | // collect all data for lakka 41 | _statusCode = _httpRequest.status; 42 | _responseText = _httpRequest.responseText; 43 | _options = { 44 | "headers": { 45 | "Cache-Control": _httpRequest.getResponseHeader("Cache-Control"), 46 | "Content-Type": _httpRequest.getResponseHeader("Content-Type"), 47 | "Expires": _httpRequest.getResponseHeader("Expires") 48 | } 49 | } 50 | try { 51 | // yay! the content from the remote location is stored it in the cache 52 | _cacheItem = window.lakka.after(uri, _responseText, _statusCode, _options); 53 | _cacheItem.statusText = "remote"; 54 | } catch (err) { 55 | // oh :( it's not cachable 56 | // create an object mimicing the lakka cache item 57 | // include the original response object 58 | _cacheItem = { 59 | "httpRequest": _httpRequest, 60 | "status": _httpRequest.status, 61 | "responseText": _httpRequest.responseText, 62 | "statusText": "remote" 63 | }; 64 | } 65 | // pass the lakka item (or its look-alike) to the callback 66 | return callback(_cacheItem); 67 | } 68 | }; 69 | 70 | _httpRequest.open("GET", uri); 71 | _httpRequest.send(); 72 | }; 73 | 74 | // define a friendly location with CORS headers 75 | // proxy from https://github.com/martinkr/corsify 76 | var uri = 'http://localhost:3001/http:/shaky-library.surge.sh'; 77 | 78 | // make the lakka-enhanced request 79 | lakkaXHR(uri, function callback(response) { 80 | console.log("Received response from " + response.statusText, ": ", response.responseText.slice(0, 45) + "... "); 81 | }); 82 | -------------------------------------------------------------------------------- /app/facade/sessionstorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module facade/sessionstorage 3 | * @exports get () 4 | * @exports set () 5 | * @exports del () 6 | * @exports has () 7 | * @description 8 | * The window.sessionstorage facade 9 | * This module provides a facade for accessing the internal private cache. 10 | * In this case: we're using the sessionStorage 11 | * 12 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 13 | * @license MIT license: https://opensource.org/licenses/MIT 14 | * 15 | * @author Martin Krause 16 | */ 17 | 18 | // imports 19 | 20 | /** 21 | * Entry poing for all API functions. 22 | * Distributes the call to the appropriate action 23 | * @snyc 24 | * @memberof facade/sessionstorage 25 | * @param {String} action 26 | * @param {String} key the key for the sessionStorage item to manipulate 27 | * @param {String} [value] the value for setting a value 28 | * @return {Any} the sessionStorages response 29 | */ 30 | const _proxy = (action, key, value) => { 31 | if (!action || !key) { 32 | throw new Error("Missing arguments"); 33 | } 34 | try { 35 | switch (action) { 36 | 37 | case "set": 38 | if (!value) { 39 | throw new Error("Missing arguments"); 40 | } 41 | return window.sessionStorage.setItem(key, value); 42 | 43 | case "get": 44 | return window.sessionStorage.getItem(key); 45 | 46 | case "del": 47 | return window.sessionStorage.removeItem(key); 48 | 49 | case "has": 50 | return Boolean(window.sessionStorage.getItem(key)); 51 | 52 | } 53 | } catch (err) { 54 | throw new Error(err); 55 | } 56 | 57 | } 58 | 59 | 60 | // API 61 | const api = { 62 | 63 | /** 64 | * Retrives a value for a given key or null 65 | * @snyc 66 | * @private 67 | * @memberof facade/sessionstorage 68 | * @param {String} key the key for the sessionStorage item to get 69 | * @return {Any} the sessionStorages response, null if there's no item 70 | */ 71 | "get": (key) => _proxy("get", key), 72 | 73 | /** 74 | * Stores a key/value pair 75 | * @snyc 76 | * @private 77 | * @memberof facade/sessionstorage 78 | * @param {String} key the key for the sessionStorage item to manipulate 79 | * @param {String} value the value for setting a value 80 | * @return {Any} the sessionStorages response 81 | */ 82 | "set": (key, value) => _proxy("set", key, value), 83 | 84 | /** 85 | * Deletes an item 86 | * @snyc 87 | * @private 88 | * @memberof facade/sessionstorage 89 | * @param {String} key the key for the sessionStorage item to delete 90 | * @return {Any} the sessionStorages response 91 | */ 92 | "del": (key) => _proxy("del", key), 93 | 94 | /** 95 | * Returns a boolean value indicating if there's an entry for this key 96 | * @snyc 97 | * @api 98 | * @memberof facade/sessionstorage 99 | * @param {String} key the key for the sessionStorage item to check 100 | * @return {Boolean} 101 | */ 102 | "has": (key) => _proxy("has", key), 103 | }; 104 | 105 | export default api; -------------------------------------------------------------------------------- /app/facade/localstorage__async.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module facade/localstorage 3 | * @exports get async() 4 | * @exports set async() 5 | * @exports del async() 6 | * @exports has async() 7 | * @description 8 | * The window.localstorage facade 9 | * This module provides a facade for accessing the internal private cache. 10 | * In this case: we're using the localStorage 11 | * 12 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 13 | * @license MIT license: https://opensource.org/licenses/MIT 14 | * 15 | * @author Martin Krause 16 | */ 17 | 18 | // imports 19 | 20 | /** 21 | * Entry poing for all API functions. 22 | * Distributes the call to the appropriate action 23 | * @asnyc 24 | * @memberof facade/localstorage 25 | * @param {String} action 26 | * @param {String} key the key for the localStorage item to manipulate 27 | * @param {String} [value] the value for setting a value 28 | * @return {Any} the localStorages response 29 | */ 30 | const _proxy = async (action, key, value) => { 31 | if (!action || !key) { 32 | throw new Error("Missing arguments"); 33 | } 34 | try { 35 | switch (action) { 36 | 37 | case "set": 38 | if (!value) { 39 | throw new Error("Missing arguments"); 40 | } 41 | return window.localStorage.setItem(key, value); 42 | 43 | case "get": 44 | return window.localStorage.getItem(key); 45 | 46 | case "del": 47 | return window.localStorage.removeItem(key); 48 | 49 | case "has": 50 | return Boolean(window.localStorage.getItem(key)); 51 | 52 | } 53 | } catch (err) { 54 | throw new Error(err); 55 | } 56 | 57 | } 58 | 59 | 60 | // API 61 | const api = { 62 | 63 | /** 64 | * Retrives a value for a given key or null 65 | * @asnyc 66 | * @private 67 | * @memberof facade/localstorage 68 | * @param {String} key the key for the localStorage item to get 69 | * @return {Any} the localStorages response, null if there's no item 70 | */ 71 | "get": async (key) => await _proxy("get", key), 72 | 73 | /** 74 | * Stores a key/value pair 75 | * @asnyc 76 | * @private 77 | * @memberof facade/localstorage 78 | * @param {String} key the key for the localStorage item to manipulate 79 | * @param {String} value the value for setting a value 80 | * @return {Any} the localStorages response 81 | */ 82 | "set": async (key, value) => await _proxy("set", key, value), 83 | 84 | /** 85 | * Deletes an item 86 | * @asnyc 87 | * @private 88 | * @memberof facade/localstorage 89 | * @param {String} key the key for the localStorage item to delete 90 | * @return {Any} the localStorages response 91 | */ 92 | "del": async (key) => await _proxy("del", key), 93 | 94 | /** 95 | * Returns a boolean value indicating if there's an entry for this key 96 | * @asnyc 97 | * @api 98 | * @memberof facade/localstorage 99 | * @param {String} key the key for the localStorage item to check 100 | * @return {Boolean} 101 | */ 102 | "has": async (key) => await _proxy("has", key), 103 | }; 104 | 105 | 106 | export default api; -------------------------------------------------------------------------------- /example/fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function fetches remote data 3 | * if there's no item in the lakka-cache and 4 | * stores the received data afterwards. 5 | * It's a basic example how to integrate lakka 6 | * into your scripts using fetch(). 7 | * Assumes you already included "../dist/lakka.js" 8 | * so window.lakka is available. 9 | * @param {String} The URI to call 10 | * @return {Promise} A promise of a lakka item 11 | */ 12 | var lakkaFetch = function (uri) { 13 | var _request; 14 | var _response; 15 | var _options; 16 | var _headers; 17 | // setup the fetch 18 | _headers = new Headers(); 19 | _headers.append("Accept", "text/plain"); 20 | _options = { 21 | method: "GET", 22 | headers: _headers, 23 | mode: "cors" 24 | }; 25 | // get the cached content from lakka 26 | try { 27 | // yay! content from the cache 28 | _response = window.lakka.before(uri, {}); 29 | // retun a resolved promise 30 | return Promise.resolve(_response); 31 | } catch (err) { 32 | // lakka throws an error if it can not return a fresh cache item 33 | // don't worry, just proceed to the request 34 | _request = new Request(uri); 35 | 36 | // return the fetch promise 37 | return fetch(_request, _options) 38 | 39 | /** Extract the response text, preserve the response */ 40 | .then(function (response) { 41 | return response.text() 42 | .then(function (content) { 43 | return { 44 | "responseText": content, 45 | "response": response 46 | } 47 | }); 48 | }) 49 | 50 | /** Handle the response, push it throught the lakka cache */ 51 | .then(function (data) { 52 | var _uri, _statusCode, _responseText, _options, _cacheItem; 53 | 54 | // collect all data for lakka 55 | _uri = data.response.url; 56 | _statusCode = data.response.status; 57 | _responseText = data.responseText; 58 | _options = { 59 | "headers": { 60 | "Cache-Control": data.response.headers.get("Cache-Control"), 61 | "Content-Type": data.response.headers.get("Content-Type"), 62 | "Expires": data.response.headers.get("Expires") 63 | } 64 | }; 65 | try { 66 | // yay! the content from the remote location is stored it in the cache 67 | _cacheItem = window.lakka.after(_uri, _responseText, _statusCode, _options); 68 | _cacheItem.statusText = "remote"; 69 | return _cacheItem; 70 | } catch (err) { 71 | // oh :( it's not cachable 72 | // create an object mimicing the lakka cache item 73 | // include the original response object 74 | _cacheItem = { 75 | "response": data.response, 76 | "status": _statusCode, 77 | "responseText": data.responseText, 78 | "statusText": "remote" 79 | }; 80 | return _cacheItem; 81 | 82 | } 83 | }); 84 | } 85 | }; 86 | 87 | // define a friendly location with CORS headers 88 | // proxy from https://github.com/martinkr/corsify 89 | var uri = 'http://localhost:3001/http:/shaky-library.surge.sh'; 90 | 91 | // make the lakka-enhanced request 92 | lakkaFetch(uri).then(function callback(response) { 93 | console.log("Received response from " + response.statusText, ": ", response.responseText.slice(0, 45) + "... "); 94 | }); 95 | -------------------------------------------------------------------------------- /test/utils/throw-invalid.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * The Specs for a small utility function that throws an error if the value evalutes to false 4 | * 5 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 6 | * @license MIT license: https://opensource.org/licenses/MIT 7 | * 8 | * @author Martin Krause 9 | */ 10 | 11 | /* eslint-env mocha */ 12 | 13 | import thisModule from "./../../app/utils/throw-invalid"; 14 | const thisModulePath = "utils/throw-invalid"; 15 | describe(`The module "${thisModulePath}"`, () => { 16 | 17 | describe("should provide an unified API. It:", () => { 18 | 19 | it("should export a sync function ", () => { 20 | thisModule.should.be.a("function"); 21 | }); 22 | }); 23 | 24 | describe("should work as expected. It:", () => { 25 | 26 | it("should throw if the value is \"false\"", (() => { 27 | try { 28 | thisModule(false); 29 | } catch (err) { 30 | err.should.be.an("error"); 31 | return true; 32 | } 33 | throw new Error("Failed"); 34 | })); 35 | 36 | it("should throw if the value is \"undefined\"", (() => { 37 | try { 38 | thisModule(undefined); 39 | } catch (err) { 40 | err.should.be.an("error"); 41 | return true; 42 | } 43 | throw new Error("Failed"); 44 | })); 45 | 46 | it("should throw if the value is \"null\"", (() => { 47 | try { 48 | thisModule(null); 49 | } catch (err) { 50 | err.should.be.an("error"); 51 | return true; 52 | } 53 | throw new Error("Failed"); 54 | })); 55 | 56 | it("should throw if the value is \"\"", (() => { 57 | try { 58 | thisModule(); 59 | } catch (err) { 60 | err.should.be.an("error"); 61 | return true; 62 | } 63 | throw new Error("Failed"); 64 | })); 65 | 66 | it("should throw if the value is an \"Error\"", (() => { 67 | try { 68 | thisModule(new Error()); 69 | } catch (err) { 70 | err.should.be.an("error"); 71 | return true; 72 | } 73 | throw new Error("Failed"); 74 | })); 75 | 76 | it("should not throw if the value is \"true\"", (() => { 77 | try { 78 | thisModule(true); 79 | } catch (err) { 80 | throw new Error("Failed"); 81 | } 82 | return true; 83 | })); 84 | 85 | it("should not throw if the value is a \"Number\"", (() => { 86 | try { 87 | thisModule(100); 88 | } catch (err) { 89 | throw new Error("Failed"); 90 | } 91 | return true; 92 | })); 93 | 94 | it("should not throw if the value is a \"String\"", (() => { 95 | try { 96 | thisModule("string"); 97 | } catch (err) { 98 | throw new Error("Failed"); 99 | } 100 | return true; 101 | })); 102 | 103 | it("should not throw if the value is an \"Array\"", (() => { 104 | try { 105 | thisModule([]); 106 | } catch (err) { 107 | throw new Error("Failed"); 108 | } 109 | return true; 110 | })); 111 | 112 | it("should not throw if the value is an \"Object\"", (() => { 113 | try { 114 | thisModule({}); 115 | } catch (err) { 116 | throw new Error("Failed"); 117 | } 118 | return true; 119 | })); 120 | 121 | it("should not throw if the value is 0", (() => { 122 | try { 123 | thisModule(0); 124 | } catch (err) { 125 | throw new Error("Failed"); 126 | } 127 | return true; 128 | })); 129 | 130 | 131 | }); 132 | 133 | }); 134 | -------------------------------------------------------------------------------- /app/facade/localstorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module facade/localstorage 3 | * @exports get() 4 | * @exports set() 5 | * @exports del() 6 | * @exports has() 7 | * @description 8 | * The window.localstorage facade 9 | * This module provides a facade for accessing the internal private cache. 10 | * In this case: we're using the localStorage 11 | * 12 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 13 | * @license MIT license: https://opensource.org/licenses/MIT 14 | * 15 | * @author Martin Krause 16 | */ 17 | 18 | // imports 19 | 20 | /** 21 | * Entry poing for all API functions. 22 | * Distributes the call to the appropriate action 23 | * @snyc 24 | * @memberof facade/localstorage 25 | * @param {String} action 26 | * @param {String} key the key for the localStorage item to manipulate 27 | * @param {String} [value] the value for setting a value 28 | * @return {Any} the localStorages response 29 | */ 30 | const _proxy = (action, key, value) => { 31 | let _item; 32 | let _objLakka; 33 | if (!action || !key) { 34 | throw new Error(); 35 | } 36 | 37 | try { 38 | switch (action) { 39 | 40 | case "set": 41 | try { 42 | _objLakka = JSON.parse(window.localStorage.getItem("lakka")); 43 | } catch (err) { 44 | // safety net 45 | } 46 | 47 | if (!_objLakka) { 48 | _objLakka = {}; 49 | } 50 | 51 | if ((!value && typeof (value) !== "string") && value !== null) { 52 | throw new Error(); 53 | } 54 | 55 | // if (typeof (value) !== "string") { 56 | // value = JSON.stringify(value); 57 | // } 58 | 59 | // API: del() 60 | if (value === null) { 61 | delete _objLakka[key]; 62 | } else { 63 | _objLakka[key] = value; 64 | } 65 | 66 | window.localStorage.setItem("lakka", JSON.stringify(_objLakka)); 67 | return true; 68 | 69 | 70 | case "get": 71 | try { 72 | _objLakka = JSON.parse(window.localStorage.getItem("lakka")); 73 | } catch (err) { 74 | _objLakka = {} 75 | return null; 76 | } 77 | _item = _objLakka[key] || null; 78 | return _item; 79 | 80 | case "del": 81 | return _proxy("set", key, null); 82 | 83 | case "flush": 84 | return window.localStorage.setItem("lakka", JSON.stringify({})); 85 | } 86 | } catch (err) { 87 | throw new Error(err); 88 | } 89 | 90 | } 91 | 92 | 93 | // API 94 | const api = { 95 | 96 | /** 97 | * Retrives a value for a given key or null 98 | * @snyc 99 | * @private 100 | * @memberof facade/localstorage 101 | * @param {String} key the key for the localStorage item to get 102 | * @return {Any} the localStorages response, null if there's no item 103 | */ 104 | "get": (key) => _proxy("get", key), 105 | 106 | /** 107 | * Stores a key/value pair 108 | * @snyc 109 | * @private 110 | * @memberof facade/localstorage 111 | * @param {String} key the key for the localStorage item to manipulate 112 | * @param {String} value the value for setting a value 113 | * @return {Any} the localStorages response 114 | */ 115 | "set": (key, value) => _proxy("set", key, value), 116 | 117 | /** 118 | * Deletes an item 119 | * @snyc 120 | * @private 121 | * @memberof facade/localstorage 122 | * @param {String} key the key for the localStorage item to delete 123 | * @return {Any} the localStorages response 124 | */ 125 | "del": (key) => _proxy("del", key), 126 | 127 | /** 128 | * Flushes the complete cache 129 | * @snyc 130 | * @private 131 | * @memberof facade/localstorage 132 | * @return {Any} the localStorages response 133 | */ 134 | "flush": () => _proxy("flush", "*") 135 | 136 | }; 137 | 138 | 139 | export default api; -------------------------------------------------------------------------------- /app/cache/calculate-validity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module cache/calculate-validity 3 | * @exports a sync function 4 | * 5 | * @description 6 | * Calculates the timestamp until this cache entry is valid 7 | * Looks at the Cache-Control Header, the Expires header and finally the default value. 8 | * Returns a timestamp in ms 9 | * 10 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 11 | * @license MIT license: https://opensource.org/licenses/MIT 12 | * 13 | * @author Martin Krause 14 | */ 15 | 16 | /** 17 | * Calculates the value from the Cache-Control Header. 18 | * Throws an Error if this is not possible. 19 | * @sync 20 | * @memberof cache/calculate-validity 21 | * @param {String} cacheControlHeaderValue the value of the Cache-Control Header 22 | * @return {Number|Error} the timestamp until the request should be cached or an Error 23 | */ 24 | const _fromCacheControlHeader = (cacheControlHeaderValue, now) => { 25 | if (!cacheControlHeaderValue || !now) { 26 | throw new Error() 27 | } 28 | const regex = /max-age=([0-9]*)/g; 29 | let _exec = regex.exec(cacheControlHeaderValue); 30 | 31 | // no max-age given, look at expires and default 32 | if (!_exec || !_exec[1]) { 33 | throw new Error(); 34 | } 35 | 36 | // max-age given, is it a number? based on the regexp is has to be ... 37 | let _maxAge; 38 | try { 39 | _maxAge = Number(_exec[1]); 40 | } catch (err) { 41 | /* istanbul ignore next */ 42 | throw new Error(); 43 | } 44 | // but it could be empty ... 45 | return Number(now + _maxAge); 46 | }; 47 | 48 | /** 49 | * Calculates the value from the Expires Header. 50 | * Throws an Error if this is not possible. 51 | * @sync 52 | * @memberof cache/calculate-validity 53 | * @param {String} expiresHeaderValue the value of the Expires Header 54 | * @return {Number|Error} the timestamp until the request should be cached or an Error 55 | */ 56 | const _fromExpiresHeader = (expiresHeaderValue) => { 57 | if (!expiresHeaderValue || Number(expiresHeaderValue) <= 0) { 58 | throw new Error(); 59 | } 60 | // convert datestring to timestamp 61 | let _expiresTimestamp = new Date(expiresHeaderValue).getTime(); 62 | // check conversion: needs to be numberic, a number, and bigger than zero 63 | if (new RegExp("/^[0-9]*$/gm").test(_expiresTimestamp) || isNaN(_expiresTimestamp) || _expiresTimestamp < 0) { 64 | throw new Error(); 65 | } 66 | return _expiresTimestamp; 67 | }; 68 | 69 | 70 | /** 71 | * Calculates the value until this request should be cached. 72 | * Looks at the Cache-Control Header (max-age), the Expires Header 73 | * and falls back to the default validity from the config 74 | * @sync 75 | * @memberof cache/calculate-validity 76 | * @param {String} cacheControlHeaderValue the value of the Cache-Control Header 77 | * @param {String} expiresHeaderValue the value of the Expires Header 78 | * @param {String} defaultValidity the value of the default validity from the configuration 79 | * @return {Number|Error} the timestamp until the request should be cached 80 | */ 81 | const main = (cacheControlHeaderValue, expiresHeaderValue, defaultValidity) => { 82 | // return new Date().getTime() + defaultValidity; 83 | let _now = new Date().getTime(); 84 | 85 | // cache-control header 86 | try { 87 | return _fromCacheControlHeader(cacheControlHeaderValue, _now) 88 | } catch (err) { 89 | // eslint-disable-line no-empty 90 | // catch error, nothing to see, move on 91 | } 92 | 93 | // expires header 94 | try { 95 | return _fromExpiresHeader(expiresHeaderValue); 96 | } catch (err) { 97 | // eslint-disable-line no-empty 98 | // catch error, nothing to see, move on 99 | } 100 | 101 | // no success - use default value 102 | return Number(_now + defaultValidity); 103 | }; 104 | 105 | export default main; -------------------------------------------------------------------------------- /app/api/before.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module api/before 4 | * @exports a sync function 5 | * @returns {Object|Error} Returns the cached response item or throws an Error if the cache is empy or stale 6 | * 7 | * @description 8 | * Handles all the caching steps to be done BEFORE sending a request: 9 | * 10 | * check the include / exclude pattern or throw. 11 | * check the cache-control Header or throw. 12 | * check the accept Header or throw. 13 | * check the contentType Header or throw. 14 | * create the $KEY with escape() 15 | * check if the localStorage contains a $RESPONSE for $KEY or throw. 16 | * check for a stale cache by looking at the $TIMESTAMP or throw. 17 | * return the cached $RESPONSE for $KEY in appropritate format if it's not stale 18 | * or throw and make the request because the cache is stale or empty or not used 19 | * 20 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 21 | * @license MIT license: https://opensource.org/licenses/MIT 22 | * 23 | * @author Martin Krause 24 | */ 25 | 26 | import configuration from "./../configuration/main"; 27 | import patternMatch from "./../pattern-match/main"; 28 | import validCacheControl from "./../header/valid-cache-control"; 29 | import validContentType from "./../header/valid-content-type"; 30 | import cache from "./../facade/localstorage"; 31 | import createKey from "./../cache/create-key"; 32 | import checkHeaderValue from "./../header/utils/check-value"; 33 | import getFromCache from "./../cache/get-item"; 34 | 35 | /** 36 | * Checks if the given URI is part of the include / exclude pattern or throws an Error. 37 | * @param {String} uri the uri for the request 38 | * @return {Boolean|Error} throws an Error if we should ignore this uri 39 | */ 40 | const _checkPatterns = (uri) => { 41 | 42 | // get everything from the config 43 | const _include = configuration.get("include"); 44 | const _exclude = configuration.get("exclude"); 45 | 46 | // the exclude-pattern takes precedent over the include-pattern 47 | if (_exclude.length && patternMatch(uri, _exclude)) { 48 | throw new Error(); 49 | } 50 | 51 | // the include-pattern must be a match if there's an include pattern 52 | // otherwise the include-pattern is not important 53 | if (_include.length && patternMatch(uri, _include) === false) { 54 | throw new Error(); 55 | } 56 | 57 | // no pattern or no failures 58 | return true; 59 | }; 60 | 61 | /** 62 | * Handles all the caching steps to be done BEFORE sending a request: 63 | * 64 | * check the include / exclude pattern 65 | * check the cache-control Header 66 | * check the accept Header 67 | * check the contentType Header 68 | * create the $KEY with escape() 69 | * check if the localStorage contains a $RESPONSE for $KEY 70 | * check for a stale cache by looking at the $TIMESTAMP 71 | * return the cached $RESPONSE for $KEY in appropritate format if it's not stale 72 | * or throw and make the request because the cache is stale or empty or not used 73 | * 74 | * @param {String} uri the uri 75 | * @param {Object} options the options for this request. eg options.headers.Content-Type 76 | * @return {Object|Error} the cached item or an Error if this url does not have an item which is stil fresh 77 | */ 78 | const main = (uri, options) => { 79 | // we're only accepting strings as the first and an optional object as second parameter 80 | if (typeof (uri) !== "string" || (options && options instanceof Object && options.constructor === Object) === false) { 81 | throw new Error(); 82 | } 83 | try { 84 | // we need to check if the given URI is part of the include / exclude pattern or throw. 85 | _checkPatterns(uri); 86 | // check if the cache control header let's us handle this request 87 | checkHeaderValue(validCacheControl)(options)("Cache-Control"); 88 | // check if the accept header let's us handle this request 89 | checkHeaderValue(validContentType)(options)("Accept"); 90 | // check if the content-type header let's us handle this request 91 | checkHeaderValue(validContentType)(options)("Content-Type"); 92 | // check the cache for cached content which is still fresh 93 | return getFromCache(cache)(createKey(uri)); 94 | 95 | } catch (err) { 96 | throw new Error(); 97 | } 98 | 99 | }; 100 | export default main; -------------------------------------------------------------------------------- /test/header/utils/get-value.spec.js: -------------------------------------------------------------------------------- 1 | // /** 2 | // * The Specs for a curried function returning the value or "defaultValue" 3 | // * for the header-property "which" from the "headers" object. 4 | // * Handles the the default value fallback and lowercase properties 5 | // * 6 | // * 7 | // * @copyright 2017 Martin Krause (http://martinkr.github.io) 8 | // * @license MIT license: https://opensource.org/licenses/MIT 9 | // * 10 | // * @author Martin Krause 11 | // */ 12 | /** 13 | * 14 | * The Specs for a curried function returning if the value for a "header" property 15 | * inside the "options.headers" object is valid as decided by "fn" 16 | * 17 | * 18 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 19 | * @license MIT license: https://opensource.org/licenses/MIT 20 | * 21 | * @author Martin Krause 22 | */ 23 | 24 | /* eslint-env mocha */ 25 | 26 | import thisModule from "./../../../app/header/utils/get-value"; 27 | const thisModulePath = "header/utils/get-value"; 28 | 29 | let headers = { 30 | "X-Foo": "foo", 31 | "X-Bar": "bar" 32 | }; 33 | 34 | const foobar = "foobar"; 35 | 36 | describe(`The module "${thisModulePath}"`, () => { 37 | 38 | 39 | afterEach((done) => { 40 | done(); 41 | }); 42 | 43 | beforeEach((done) => { 44 | headers = { 45 | "X-Foo": "foo", 46 | "X-Bar": "bar" 47 | }; 48 | done(); 49 | }); 50 | 51 | describe("should provide an unified API. It:", () => { 52 | 53 | it("should export a sync function ", () => { 54 | thisModule.should.be.a("function"); 55 | }); 56 | }); 57 | 58 | describe("should take up to three parameters (curried function). It:", () => { 59 | 60 | it("should return \"null\" if the second curried function's param is omitted", (() => { 61 | let _result = thisModule(headers)()(foobar); 62 | (_result === null).should.be.true; 63 | })); 64 | 65 | it("should return \"null\" if the first curried function's param and the third curried function's param are omitted", (() => { 66 | let _result = thisModule()("X-Foo")(); 67 | (_result === null).should.be.true; 68 | })); 69 | 70 | it("should return \"null\" if the first curried function's param and the second curried function's param and the third curried function's param are omitted", (() => { 71 | let _result = thisModule()()(); 72 | (_result === null).should.be.true; 73 | })); 74 | 75 | }); 76 | 77 | describe("should use the thirds function's param as default value. It:", () => { 78 | 79 | it("should return the default value if there's one and the first curried function's param is omitted", (() => { 80 | thisModule()("X-Foo")(foobar).should.equal(foobar); 81 | })); 82 | 83 | it("should return the default value if there's one and the first curried function's param and the second curried function's param are omitted", (() => { 84 | thisModule()()(foobar).should.equal(foobar); 85 | })); 86 | 87 | 88 | }); 89 | 90 | describe("should work as expected. It:", () => { 91 | 92 | it("should return the value if there's a value at the first curried function's parameters object under the seconds curried function's parameter as key", (() => { 93 | thisModule(headers)("X-Bar")().should.be.equal(headers["X-Bar"]); 94 | })); 95 | 96 | it("should return the value if there's a value and the value passes the first curried function's parameter condition ", (() => { 97 | delete headers["X-Bar"]; 98 | headers["x-bar"] = "foo"; 99 | thisModule(headers)("X-Bar")().should.be.equal(headers["x-bar"]); 100 | })); 101 | 102 | 103 | it("should return the default value if there's a third curried function parameter and no value at the first curried function's parameters object under the seconds curried function's parameter as key", (() => { 104 | delete headers["X-Bar"]; 105 | thisModule(headers)("X-Bar")(foobar).should.be.equal(foobar); 106 | })); 107 | 108 | it("should return null if there's no third curried function parameter and no value at the first curried function's parameters object under the seconds curried function's parameter as key", (() => { 109 | delete headers["X-Bar"]; 110 | let _result = thisModule(headers)("X-Bar")(); 111 | (_result === null).should.be.true; 112 | })); 113 | 114 | 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/header/utils/check-value.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * The Specs for a curried function returning if the value for a "header" property 4 | * inside the "options.headers" object is valid as decided by "fn" 5 | * 6 | * 7 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 8 | * @license MIT license: https://opensource.org/licenses/MIT 9 | * 10 | * @author Martin Krause 11 | */ 12 | 13 | /* eslint-env mocha */ 14 | 15 | import thisModule from "./../../../app/header/utils/check-value"; 16 | const thisModulePath = "header/utils/check-value"; 17 | 18 | let fn = { 19 | "fooFn": (value) => value === "foo" 20 | } 21 | 22 | let options = { 23 | "headers": { 24 | "X-Foo": "foo", 25 | "X-Bar": "bar", 26 | } 27 | } 28 | 29 | describe(`The module "${thisModulePath}"`, () => { 30 | 31 | let spy = sinon.spy(fn, "fooFn"); 32 | 33 | afterEach((done) => { 34 | spy.resetHistory(); 35 | done(); 36 | }); 37 | 38 | after((done) => { 39 | spy.restore(); 40 | done(); 41 | }); 42 | 43 | beforeEach((done) => { 44 | options = { 45 | "headers": { 46 | "X-Foo": "foo", 47 | "X-Bar": "bar", 48 | } 49 | }; 50 | done(); 51 | }); 52 | 53 | describe("should provide an unified API. It:", () => { 54 | 55 | it("should export a sync function ", () => { 56 | thisModule.should.be.a("function"); 57 | }); 58 | }); 59 | 60 | describe("should take up to three parameters (curried function). It:", () => { 61 | 62 | it("should throw if the first curried function's param is omitted", (() => { 63 | try { 64 | thisModule()()(); 65 | } catch (err) { 66 | err.should.be.an("error"); 67 | return true; 68 | } 69 | throw new Error("Failed"); 70 | })); 71 | 72 | it("should throw if the first curried function's param is not a \"Function\"", (() => { 73 | try { 74 | thisModule(1)()(); 75 | } catch (err) { 76 | err.should.be.an("error"); 77 | return true; 78 | } 79 | throw new Error("Failed"); 80 | })); 81 | 82 | it("should return \"true\" if the second curried function's param is omitted", (() => { 83 | thisModule(fn.fooFn)()("X-Foo").should.be.true; 84 | thisModule(fn.fooFn)()().should.be.true; 85 | })); 86 | 87 | it("should return \"true\" if the second curried function's param does not have a headers-propertry", (() => { 88 | delete options.headers; 89 | thisModule(fn.fooFn)(options)("X-Foo").should.be.true; 90 | })); 91 | 92 | it("should return \"true\" if the third curried function's param is omitted", (() => { 93 | thisModule(fn.fooFn)(options)().should.be.true; 94 | thisModule(fn.fooFn)()().should.be.true; 95 | })); 96 | }); 97 | 98 | describe("should work as expected. It:", () => { 99 | 100 | it("should call the first curried function's parameter with the value in the second function's parameter's object stored under the third function's parameter as key", (() => { 101 | thisModule(fn.fooFn)(options)("X-Foo"); 102 | spy.should.have.been.calledWith(options.headers["X-Foo"]); 103 | })); 104 | 105 | it("should return \"true\" and not throw if there's a value and the value passes the first curried function's parameter condition ", (() => { 106 | thisModule(fn.fooFn)(options)("X-Foo").should.be.true; 107 | })); 108 | 109 | it("should return \"true\" if there's no value from the second and third parameter", (() => { 110 | options.headers["X-Empty"] = null; 111 | thisModule(fn.fooFn)(options)("X-Empty").should.be.true; 112 | })); 113 | 114 | it("should return \"true\" if there's no property with third parameter at the second parameter's object", (() => { 115 | delete options.headers["X-Empty"]; 116 | thisModule(fn.fooFn)(options)("X-Empty").should.be.true; 117 | })); 118 | 119 | it("should return \"true\" if there's a value and the value passes the first curried function's parameter condition and the key is lowercase ", (() => { 120 | delete options.headers["X-Bar"]; 121 | options.headers["x-bar"] = "foo"; 122 | thisModule(fn.fooFn)(options)("X-Bar").should.be.true; 123 | })); 124 | 125 | it("should not throw if there's a value and the value does not pass the first curried function's parameter condition ", (() => { 126 | try { 127 | thisModule(fn.fooFn)(options)("X-Bar"); 128 | } catch (err) { 129 | err.should.be.an("error"); 130 | return true; 131 | } 132 | throw new Error("Failed"); 133 | })); 134 | 135 | }); 136 | 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /app/configuration/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module configuration/main 3 | * @description 4 | * This module provides the funcitonality to configure the lakka-cache. 5 | * @exports get () 6 | * @exports set () 7 | * 8 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 9 | * @license MIT license: https://opensource.org/licenses/MIT 10 | * 11 | * @author Martin Krause 12 | */ 13 | 14 | // imports 15 | import defaults from './default'; 16 | const validProperties = ["include", "exclude", "minutes"]; 17 | 18 | let config = Object.assign({}, defaults); 19 | 20 | /** 21 | * Verifies if the arguments passed to ".set()" are valid. 22 | * @param {Array} args the arguments passed to .set() 23 | * @return {Boolean} 24 | */ 25 | const _validArguments = (args) => { 26 | 27 | // no arguments 28 | if (args.length === 0) { 29 | return false; 30 | } 31 | // just one argument but not an Object ... 32 | if (args.length === 1 && args[0] !== Object(args[0])) { 33 | return false; 34 | } 35 | 36 | // just one argument which is an object: check if the object contains only valid properties 37 | if (args.length === 1 && args[0] === Object(args[0])) { 38 | return Object.keys(args[0]).every((element) => validProperties.includes(element)); 39 | } 40 | 41 | // two strings as arguments: we're checking the "minutes" property first 42 | if (args.length === 2 && args[0] === "minutes") { 43 | if ( 44 | // should be a number or parsable to a number 45 | (typeof (Number(args[1])) === "number" && Number.isNaN(Number(args[1])) === false) 46 | // but not an array 47 | && (Array.isArray(args[1]) === false) 48 | // but not a boolean 49 | && (typeof (args[1]) !== "boolean") 50 | ) { 51 | return true; 52 | } else { 53 | return false; 54 | } 55 | } 56 | 57 | // two string arguments: check if the first one refers to a valid property 58 | if (args.length === 2 && typeof (args[0]) === "string" && typeof (args[1]) === "string") { 59 | return validProperties.includes(args[0]); 60 | } 61 | 62 | return false; 63 | }; 64 | 65 | /** 66 | * Retrives a value for a given key or null 67 | * @sync 68 | * @private 69 | * @memberof configuration/main 70 | * @param {String} key the key for the sessionStorage item to get 71 | * @return {Any} the configuration value 72 | */ 73 | const _get = (key) => { 74 | if (!key || typeof (key) !== "string" || validProperties.includes(key) !== true) { 75 | throw new Error(); 76 | } 77 | return config[key]; 78 | } 79 | 80 | 81 | /** 82 | * Stores a key/value pair 83 | * @sync 84 | * @private 85 | * @memberof configuration/main 86 | * @param {String|Object} key the key for the value OR the configuration object 87 | * @param {String} [value] the value to set. Skip if the first argument is a configuraiton object 88 | * @return {Any} the sessionStorages response 89 | */ 90 | const _set = (key, value) => { 91 | // unique 92 | let args = [key, value].filter((element) => element); 93 | 94 | if (!_validArguments(args)) { 95 | throw new Error(); 96 | } 97 | 98 | // object insted of a key-value pair: merge into current config 99 | if (args[0] === Object(args[0])) { 100 | config = Object.assign(config, args[0]); 101 | return true; 102 | } 103 | 104 | if (key === "include" || key === "exclude") { 105 | // we're storing regexp for performance reasons 106 | /* istanbul ignore next */ 107 | if (typeof (value) === "string") { 108 | value = new RegExp(value); 109 | } 110 | // ignore dublicates 111 | if (config[key].some((element) => element.source === value.source)) { 112 | return true; 113 | } 114 | config[key].push(value); 115 | return true; 116 | } 117 | 118 | // key-value pair: store 119 | config[key] = value; 120 | 121 | return true; 122 | } 123 | 124 | 125 | 126 | // API 127 | const api = { 128 | 129 | /** 130 | * Retrives a value for a given key or null 131 | * @sync 132 | * @private 133 | * @memberof configuration/main 134 | * @param {String} key the key for the sessionStorage item to get 135 | * @return {Any} the configuration value 136 | */ 137 | "get": (key) => _get(key), 138 | 139 | /** 140 | * Stores a key/value pair 141 | * @sync 142 | * @private 143 | * @memberof configuration/main 144 | * @param {String|Object} key the key for the value OR the configuration object 145 | * @param {String} [value] the value to set. Skip if the first argument is a configuraiton object 146 | * @return {Any} the sessionStorages response 147 | */ 148 | "set": (key, value) => _set(key, value) 149 | 150 | }; 151 | 152 | export default api; 153 | -------------------------------------------------------------------------------- /app/api/after.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @module api/after 4 | * @exports a sync function 5 | * @returns {String|Object|Error} Stores a cache item at the cache if it's a cacheable, valid response 6 | * 7 | * @description 8 | * Handles all the caching steps to be done AFTER receiving the request's response 9 | * 10 | * check the status code for success or throw. 11 | * check the "Cache-Control" - header or throw. 12 | * - Cache if it's "public", "private", "Immutable" 13 | * - Ignore if "must-revalidate", "no-cache", "no-store", "proxy-revalidate" 14 | * check the "Expires", "Cache-Control" - Header to see if the content is not already stale (crazy but ppl might use this to prevent caching :/ ) or throw 15 | * check the "Content-Type" or throw 16 | * create and save the cache item 17 | * return the cache or throw an error 18 | * 19 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 20 | * @license MIT license: https://opensource.org/licenses/MIT 21 | * 22 | * @author Martin Krause 23 | */ 24 | 25 | import configuration from "./../configuration/main"; 26 | 27 | import validStatusCode from "./../header/valid-status-code"; 28 | import validCacheControl from "./../header/valid-cache-control"; 29 | import calculateValidity from "./../cache/calculate-validity"; 30 | import validContentType from "./../header/valid-content-type"; 31 | import cache from "./../facade/localstorage"; 32 | import createKey from "./../cache/create-key"; 33 | import createItem from "./../cache/create-item"; 34 | import throwIfInvalid from "./../utils/throw-invalid"; 35 | import getHeaderValue from "./../header/utils/get-value"; 36 | import checkHeaderValue from "./../header/utils/check-value"; 37 | import setToCache from "./../cache/set-item"; 38 | import patternMatch from "./../pattern-match/main"; 39 | 40 | const defaultMinutes = configuration.get("minutes"); 41 | const defaultMiliseconds = defaultMinutes * 60000; 42 | /** 43 | * Checks if the given URI is part of the include / exclude pattern or throws an Error. 44 | * @param {String} uri the uri for the request 45 | * @return {Boolean|Error} throws an Error if we should ignore this uri 46 | */ 47 | const _checkPatterns = (uri) => { 48 | 49 | // get everything from the config 50 | const _include = configuration.get("include"); 51 | const _exclude = configuration.get("exclude"); 52 | 53 | // the exclude-pattern takes precedent over the include-pattern 54 | if (_exclude.length && patternMatch(uri, _exclude)) { 55 | throw new Error(); 56 | } 57 | 58 | // the include-pattern must be a match if there's an include pattern 59 | // otherwise the include-pattern is not important 60 | if (_include.length && patternMatch(uri, _include) === false) { 61 | throw new Error(); 62 | } 63 | 64 | // no pattern or no failures 65 | return true; 66 | }; 67 | 68 | 69 | /** 70 | * Handles all the caching steps to be done AFTER receiving the request's response 71 | * 72 | * check the status code for success or throw. 73 | * check the "Cache-Control" - header or throw. 74 | * - Cache if it's "public", "private", "Immutable" 75 | * - Ignore if "must-revalidate", "no-cache", "no-store", "proxy-revalidate" 76 | 77 | * check the "Expires", "Cache-Control" - Header to see if the content is not already stale (crazy but ppl might use this to prevent caching :/ ) or throw 78 | * check the "Content-Type" or throw 79 | * create and save the cache item 80 | * return the cache or throw an error 81 | * 82 | * @param {String} uri the uri 83 | * @param {Object} options the options for this request. eg options.headers.Content-Type 84 | * @return {Object|Error} the cached item or an Error if this url does not have an item which is stil fresh 85 | */ 86 | const main = (uri, responseText, statusCode, options) => { 87 | 88 | // we're only accepting a String as the first, a String as the second, a Number or String as the third and an Object as the fourth parameter 89 | if (typeof (uri) !== "string" || 90 | typeof (responseText) !== "string" || 91 | (typeof (statusCode) !== "number" && typeof (statusCode) !== "string") || 92 | (options instanceof Object && options.constructor === Object) === false 93 | 94 | ) { 95 | throw new Error(); 96 | } 97 | 98 | const _statusCode = String(statusCode); 99 | 100 | try { 101 | // we need to check if the given URI is part of the include / exclude pattern or throw. 102 | _checkPatterns(uri); 103 | // check if the status code indicates a successful request 104 | throwIfInvalid(validStatusCode(_statusCode)); 105 | // check if the cache control header let's us cache this request 106 | checkHeaderValue(validCacheControl)(options)("Cache-Control"); 107 | // check if the content-type is a cachable one 108 | checkHeaderValue(validContentType)(options)("Content-Type"); 109 | // check if the expires header has a future date or there's no expires header 110 | throwIfInvalid(calculateValidity("", getHeaderValue(options.headers)("Expires")(null), defaultMiliseconds) >= Date.now()); 111 | 112 | } catch (err) { 113 | throw new Error(); 114 | } 115 | 116 | // write to cache 117 | let _item; 118 | try { 119 | _item = createItem(uri, responseText, options.headers); 120 | setToCache(cache)(createKey(uri))(_item); 121 | } catch (err) { 122 | // just a safety net 123 | /* istanbul ignore next */ 124 | throw new Error(); 125 | } 126 | 127 | return _item; 128 | 129 | }; 130 | 131 | export default main; 132 | -------------------------------------------------------------------------------- /dist/lakka.umd.js: -------------------------------------------------------------------------------- 1 | /*! Lakka, github.com/martinkr/Lakka */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.lakka=t():e.lakka=t()}(self,(function(){return(()=>{"use strict";var e={d:(t,r)=>{for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{after:()=>L,before:()=>M,configuration:()=>J,flush:()=>z,ignore:()=>D,recognize:()=>I,remove:()=>P,time:()=>A});const r={include:[],exclude:[],minutes:60};var n=["include","exclude","minutes"],o=Object.assign({},r),u=function(e,t){var r=[e,t].filter((function(e){return e}));if(!function(e){return 0!==e.length&&(1!==e.length||e[0]===Object(e[0]))&&(1===e.length&&e[0]===Object(e[0])?Object.keys(e[0]).every((function(e){return n.includes(e)})):2===e.length&&"minutes"===e[0]?"number"==typeof Number(e[1])&&!1===Number.isNaN(Number(e[1]))&&!1===Array.isArray(e[1])&&"boolean"!=typeof e[1]:2===e.length&&"string"==typeof e[0]&&"string"==typeof e[1]&&n.includes(e[0]))}(r))throw new Error;return r[0]===Object(r[0])?(o=Object.assign(o,r[0]),!0):"include"===e||"exclude"===e?("string"==typeof t&&(t=new RegExp(t)),o[e].some((function(e){return e.source===t.source}))||o[e].push(t),!0):(o[e]=t,!0)};const c={get:function(e){return function(e){if(!e||"string"!=typeof e||!0!==n.includes(e))throw new Error;return o[e]}(e)},set:function(e,t){return u(e,t)}};const i=function(e){return["200","203","226"].includes(e)};var s=[new RegExp("must-revalidate"),new RegExp("no-store"),new RegExp("no-cache")];const f=function(e){return"string"!=typeof e||!e||s.map((function(t){return t.test(e)})).every((function(e){return!1===e}))};const a=function(e,t,r){var n=(new Date).getTime();try{return function(e,t){if(!e||!t)throw new Error;var r,n=/max-age=([0-9]*)/g.exec(e);if(!n||!n[1])throw new Error;try{r=Number(n[1])}catch(e){throw new Error}return Number(t+r)}(e,n)}catch(e){}try{return function(e){if(!e||Number(e)<=0)throw new Error;var t=new Date(e).getTime();if(new RegExp("/^[0-9]*$/gm").test(t)||isNaN(t)||t<0)throw new Error;return t}(t)}catch(e){}return Number(n+r)};var l=[new RegExp("application/json"),new RegExp("text/x-json"),new RegExp("text/plain"),new RegExp("text/html")];const h=function(e){return!("string"!=typeof e||!e)&&l.map((function(t){return t.test(e)})).some((function(e){return!0===e}))};var w=function e(t,r,n){var o;if(!t||!r)throw new Error;try{switch(t){case"set":try{o=JSON.parse(window.localStorage.getItem("lakka"))}catch(e){}if(o||(o={}),!n&&"string"!=typeof n&&null!==n)throw new Error;// if (typeof (value) !== "string") { 3 | return null===n?delete o[r]:o[r]=n,window.localStorage.setItem("lakka",JSON.stringify(o)),!0;case"get":try{o=JSON.parse(window.localStorage.getItem("lakka"))}catch(e){return o={},null}return o[r]||null;case"del":return e("set",r,null);case"flush":return window.localStorage.setItem("lakka",JSON.stringify({}))}}catch(e){throw new Error(e)}};const g={get:function(e){return w("get",e)},set:function(e,t){return w("set",e,t)},del:function(e){return w("del",e)},flush:function(){return w("flush","*")}};const p=function(e){if("string"!=typeof e)throw new Error;return escape(e)};const y=function(e){return function(t){return function(r){if(!e&&r)return r;if(!e&&!r||!t)return null;var n=e[t]||e[t.toLowerCase()];return n||(!n&&r?r:null)}}};var d=6e4*c.get("minutes");const m=function(e,t,r){if("string"!=typeof e||"string"!=typeof t||!1===(r&&r instanceof Object&&r.constructor===Object))throw new Error;var n={headers:{}};try{n.key=p(e),n.status=200,n.statusText="cache",n.responseText=t,n.until=a(y(r)("Cache-Control")(null),y(r)("Expires")(null),d),n.headers.Status="".concat(n.status," ").concat(n.statusText),n.headers["Content-Type"]=y(r)("Content-Type")("text/plain"),n.headers["Cache-Control"]=y(r)("Cache-Control")(null),n.headers.Expires=y(r)("Expires")(null),Object.keys(n.headers).forEach((function(e){return null==n.headers[e]&&delete n.headers[e]}))}catch(e){throw new Error}return n};const E=function(e){if(!e&&0!==e||e instanceof Error)throw new Error};const b=function(e){return function(t){return function(r){if(!e||"function"!=typeof e)throw new Error;if(!t||!t.headers||!r)return!0;var n=t.headers[r]||t.headers[r.toLowerCase()];return!n||(E(e(n)),!0)}}};const x=function(e){return function(t){return function(r){E(e.set(t,r))}}};const v=function(e,t){if("string"!=typeof e||!0!==Array.isArray(t))throw new Error;return t.map((function(t){return t.test(e)})).some((function(e){return!0===e}))};var j=6e4*c.get("minutes"),O=function(e){var t=c.get("include"),r=c.get("exclude");if(r.length&&v(e,r))throw new Error;if(t.length&&!1===v(e,t))throw new Error;return!0};const C=function(e,t,r,n){if("string"!=typeof e||"string"!=typeof t||"number"!=typeof r&&"string"!=typeof r||!1==(n instanceof Object&&n.constructor===Object))throw new Error;var o,u=String(r);try{O(e),E(i(u)),b(f)(n)("Cache-Control"),b(h)(n)("Content-Type"),E(a("",y(n.headers)("Expires")(null),j)>=Date.now())}catch(e){throw new Error}try{o=m(e,t,n.headers),x(g)(p(e))(o)}catch(e){throw new Error}return o};const k=function(e){return!!(e&&e.until&&e.until>=Date.now())};const N=function(e){return function(t){var r=e.get(t);if(!k(r))throw e.del(t),new Error;return r}};var S=function(e){var t=c.get("include"),r=c.get("exclude");if(r.length&&v(e,r))throw new Error;if(t.length&&!1===v(e,t))throw new Error;return!0};const T=function(e,t){if("string"!=typeof e||!1===(t&&t instanceof Object&&t.constructor===Object))throw new Error;try{return S(e),b(f)(t)("Cache-Control"),b(h)(t)("Accept"),b(h)(t)("Content-Type"),N(g)(p(e))}catch(e){throw new Error}};var R={time:function(e){return c.set("minutes",e)},ignore:function(e){return c.set("exclude",e)},recognize:function(e){return c.set("include",e)},configuration:function(e){return c.set(e)},remove:function(e){return g.del(e)},flush:function(){return g.flush()},after:function(e,t,r,n){return C(e,t,r,n)},before:function(e,t){return T(e,t)}};var A=R.time,D=R.ignore,I=R.recognize,J=R.configuration,P=R.remove,z=R.flush,L=R.after,M=R.before;return t})()})); 4 | //# sourceMappingURL=lakka.umd.js.map -------------------------------------------------------------------------------- /app/api/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module api/main 3 | * @exports a sync function "ignore" 4 | * @exports a sync function "configuration" 5 | * 6 | * @exports a sync function "after" 7 | * @exports a sync function "before" 8 | * 9 | * @description 10 | * Defines the publicly exposed API 11 | * - ignore: 12 | * Adds an ignore pattern (a string / RegEx) to the existing / default configuration 13 | * urls matching this pattern will be ignored by the lakka cache and passed throught 14 | * - recognize: 15 | * Adds an include pattern (a string / RegEx) to the existing / default configuration 16 | * urls matching this pattern will be recognized by the lakka cache and passed throught 17 | * ig this is empty, all URIs will be recognized 18 | * - time: 19 | * Sets the minutes (Number) we should cache items for. Defaut value is 60 minutes. 20 | * Will be overwritten by the "max-age" - value of the Cache-Control Header or the Expires Header 21 | * of the response. 22 | * - configuration: 23 | * Adds a complete configuration object with "ignore", "exclude", "minutes" 24 | * - after: 25 | * Call this after the actual request. Checks the response and store it at the cache. Returns the cache item or an error. 26 | * - before: 27 | * Call this before the actual request. Checks for and returns a fresh cache item or an error. 28 | * 29 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 30 | * @license MIT license: https://opensource.org/licenses/MIT 31 | * 32 | * @author Martin Krause 33 | */ 34 | 35 | import after from "./../api/after"; 36 | import before from "./../api/before"; 37 | import cacheFacade from "./../facade/localstorage"; 38 | import configuration from "./../configuration/main"; 39 | 40 | // API 41 | const api = { 42 | 43 | /** 44 | * Sets the minutes (Number) we should cache items for. Defaut value is 60 minutes. 45 | * Will be overwritten by the "max-age" - value of the Cache-Control Header or the Expires Header 46 | * of the response. 47 | * @sync 48 | * @public 49 | * @memberof api/time 50 | * @param {Number} value the default caching time 51 | * @return {Any} the configuration value 52 | */ 53 | "time": (value) => configuration.set("minutes", value), 54 | 55 | /** 56 | * Dynamically add an "exclude" pattern to the lakka cache. 57 | * Any matches for this pattern will be ignored by the lakka cache. 58 | * @sync 59 | * @public 60 | * @memberof api/ignore 61 | * @param {String|Regex} pattern the pattern or regexp to check the uri against. Any matches will be ignored by the lakka cache. 62 | * @return {Any} the configuration value 63 | */ 64 | "ignore": (pattern) => configuration.set("exclude", pattern), 65 | 66 | /** 67 | * Dynamically add an "include" pattern to the lakka cache. 68 | * Any matches for this pattern will be ignored by the lakka cache. 69 | * @sync 70 | * @public 71 | * @memberof api/recognize 72 | * @param {String|Regex} pattern the pattern or regexp to check the uri against. Any matches will be recognized and handled by the lakka cache. 73 | * @return {Any} the configuration value 74 | */ 75 | "recognize": (pattern) => configuration.set("include", pattern), 76 | 77 | /** 78 | * Dynamically merge a configuration object into the current / default configuraiton object. 79 | * "include"/"exclude" will be merged, "minutes" will be replaced 80 | * @sync 81 | * @public 82 | * @memberof api/configuration 83 | * @param {Object} obj a configuration object 84 | * @return {Any} the configuration value 85 | */ 86 | "configuration": (obj) => configuration.set(obj), 87 | 88 | /** 89 | * Force clear an item from the cache. 90 | * @sync 91 | * @public 92 | * @memberof api/remove 93 | * @param {String} uri the uri to remove 94 | * @return {Any} the configuration value 95 | */ 96 | "remove": (uri) => cacheFacade.del(uri), 97 | 98 | 99 | /** 100 | * Flush the lakka cache. Remove all items 101 | * @sync 102 | * @public 103 | * @memberof api/configuration 104 | * @return {Any} the configuration value 105 | */ 106 | "flush": () => cacheFacade.flush(), 107 | 108 | 109 | /** 110 | * Handles all the caching steps to be done AFTER receiving the request's response 111 | * 112 | * check the status code for success or throw. 113 | * check the "Cache-Control" - header or throw. 114 | * - Cache if it's "public", "private", "Immutable" 115 | * - Ignore if "must-revalidate", "no-cache", "no-store", "proxy-revalidate" 116 | 117 | * check the "Expires", "Cache-Control" - Header to see if the content is not already stale (crazy but ppl might use this to prevent caching :/ ) or throw 118 | * check the "Content-Type" or throw 119 | * create and save the cache item 120 | * return the cache or throw an error 121 | * 122 | * @sync 123 | * @public 124 | * @memberof api/after 125 | * @param {String} uri the uri 126 | * @param {Object} options the options for this request. eg options.headers.Content-Type 127 | * @return {Object|Error} the cached item or an Error if this url does not have an item which is stil fresh 128 | */ 129 | "after": (uri, responseText, statusCode, options) => after(uri, responseText, statusCode, options), 130 | 131 | 132 | /** 133 | * Handles all the caching steps to be done BEFORE sending a request: 134 | * 135 | * check the include / exclude pattern 136 | * check the cache-control Header 137 | * check the accept Header 138 | * check the contentType Header 139 | * create the $KEY with escape() 140 | * check if the localStorage contains a $RESPONSE for $KEY 141 | * check for a stale cache by looking at the $TIMESTAMP 142 | * return the cached $RESPONSE for $KEY in appropritate format if it's not stale 143 | * or throw and make the request because the cache is stale or empty or not used 144 | * 145 | * @sync 146 | * @public 147 | * @memberof api/before 148 | * @param {String} uri the uri 149 | * @param {Object} options the options for this request. eg options.headers.Content-Type 150 | * @return {Object|Error} the cached item or an Error if this url does not have an item which is stil fresh 151 | */ 152 | "before": (uri, options) => before(uri, options) 153 | 154 | }; 155 | 156 | 157 | export default api; -------------------------------------------------------------------------------- /test/api/main.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for setting a specific item from a given cache 3 | * 4 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 5 | * @license MIT license: https://opensource.org/licenses/MIT 6 | * 7 | * @author Martin Krause 8 | */ 9 | 10 | /* eslint-env mocha */ 11 | 12 | import thisModule, { $imports } from "./../../app/api/main"; 13 | 14 | const thisModulePath = "api/main"; 15 | 16 | let stubConfiguration; 17 | let spyConfiguration; 18 | let stubAfter; 19 | let stubCache; 20 | let spyAfter; 21 | let stubBefore; 22 | let spyBefore; 23 | let spyCacheFlush; 24 | let spyCacheDel; 25 | 26 | describe(`The module "${thisModulePath}"`, () => { 27 | 28 | before((done) => { 29 | 30 | // create stubs for spying on them 31 | stubConfiguration = { "set": () => true }; 32 | spyConfiguration = sinon.spy(stubConfiguration, "set"); 33 | 34 | stubCache = { "flush": () => true, "del": () => true }; 35 | spyCacheFlush = sinon.spy(stubCache, "flush"); 36 | spyCacheDel = sinon.spy(stubCache, "del"); 37 | 38 | 39 | stubAfter = sinon.spy(); 40 | spyAfter = stubAfter; 41 | 42 | stubBefore = sinon.spy(); 43 | spyBefore = stubBefore; 44 | 45 | // $imports.$mock((source, symbol, value) => { 46 | // console.log("source: '", source, "', symbol: '", symbol, "', value: '", value, "' ") 47 | // }) 48 | // mock imports 49 | // import after from "./../api/after"; 50 | // import before from "./../api/before"; 51 | // import cacheFacade from "./../facade/localstorage"; 52 | // import configuration from "./../configuration/main"; 53 | $imports.$mock({ 54 | // replace dependencies mit spied on stubs 55 | "./../configuration/main": { 56 | default: stubConfiguration 57 | }, 58 | "./../api/after": { 59 | default: stubAfter 60 | }, 61 | "./../api/before": { 62 | default: stubBefore 63 | }, 64 | "./../facade/localstorage": { 65 | default: stubCache 66 | }, 67 | }); 68 | 69 | // thisModule = proxyquire("./../../app/" + thisModulePath, { 70 | // // replace dependencies mit spied on stubs 71 | // "./../configuration/main.js": stubConfiguration, 72 | // "./../api/after.js": stubAfter, 73 | // "./../api/before.js": stubBefore, 74 | // "./../facade/localstorage.js": stubCache 75 | // }); 76 | // console.log(`${thisModulePath} thisModule => ${thisModule}`) 77 | done(); 78 | }); 79 | 80 | beforeEach((done) => { 81 | 82 | done(); 83 | }); 84 | 85 | afterEach((done) => { 86 | stubAfter.resetHistory(); 87 | stubBefore.resetHistory(); 88 | spyConfiguration.resetHistory(); 89 | spyConfiguration.resetHistory(); 90 | done(); 91 | }); 92 | 93 | after((done) => { 94 | done(); 95 | }); 96 | 97 | describe("should provide an unified API. It:", () => { 98 | 99 | it("should export a sync function \"configuration\"", () => { 100 | thisModule.configuration.should.be.a("function"); 101 | }); 102 | 103 | it("should export a sync function \"time\"", () => { 104 | thisModule.time.should.be.a("function"); 105 | }); 106 | 107 | it("should export a sync function \"recognize\"", () => { 108 | thisModule.recognize.should.be.a("function"); 109 | }); 110 | 111 | it("should export a sync function \"ignore\"", () => { 112 | thisModule.ignore.should.be.a("function"); 113 | }); 114 | 115 | it("should export a sync function \"flush\"", () => { 116 | thisModule.flush.should.be.a("function"); 117 | }); 118 | 119 | it("should export a sync function \"after\"", () => { 120 | thisModule.after.should.be.a("function"); 121 | }); 122 | 123 | it("should export a sync function \"before\"", () => { 124 | thisModule.before.should.be.a("function"); 125 | }); 126 | 127 | }); 128 | 129 | describe("should have a working API \"time\". It:", () => { 130 | 131 | it("should call the \".set\"-function on the \"configuration\"-module with the correct - passed through - parameters", () => { 132 | thisModule.time(10) 133 | spyConfiguration.should.have.been.calledWith("minutes", 10); 134 | }); 135 | 136 | }); 137 | 138 | describe("should have a working API \"recognize\". It:", () => { 139 | 140 | it("should call the \".set\"-function on the \"configuration\"-module with the correct - passed through - parameters", () => { 141 | thisModule.recognize("testpattern") 142 | spyConfiguration.should.have.been.calledWith("include", "testpattern"); 143 | }); 144 | 145 | }); 146 | 147 | describe("should have a working API \"ignore\". It:", () => { 148 | 149 | it("should call the \".set\"-function on the \"configuration\"-module with the correct - passed through - parameters", () => { 150 | thisModule.ignore("testpattern") 151 | spyConfiguration.should.have.been.calledWith("exclude", "testpattern"); 152 | }); 153 | 154 | }); 155 | 156 | describe("should have a working API \"configuration\". It:", () => { 157 | 158 | it("should call the \".set\"-function on the \"configuration\"-module with the correct - passed through - parameters", () => { 159 | thisModule.configuration({}) 160 | spyConfiguration.should.have.been.calledWith({}); 161 | }); 162 | 163 | }); 164 | 165 | describe("should have a working API \"remove\". It:", () => { 166 | 167 | it("should call the \".del\"-function on the \"cache\"-module with the correct - passed through - parameters", () => { 168 | thisModule.remove("string") 169 | spyCacheDel.should.have.been.calledWith("string"); 170 | }); 171 | 172 | }); 173 | 174 | describe("should have a working API \"flush\". It:", () => { 175 | 176 | it("should call the \".flush\"-function on the \"cache\"-module", () => { 177 | thisModule.flush() 178 | spyCacheFlush.should.have.been.called; 179 | }); 180 | 181 | }); 182 | 183 | describe("should have a working API \"after\". It:", () => { 184 | 185 | it("should call the \"api/after\"-module with the correct - passed through - parameters", () => { 186 | thisModule.after("uri", "responseText", 200, {}); 187 | spyAfter.should.have.been.calledWithExactly("uri", "responseText", 200, {}); 188 | }); 189 | 190 | }); 191 | 192 | describe("should have a working API \"before\". It:", () => { 193 | 194 | it("should call the \"api/before\"-module with the correct - passed through - parameters", () => { 195 | thisModule.before("uri", {}); 196 | spyBefore.should.have.been.calledWith("uri", {}); 197 | }); 198 | 199 | }); 200 | 201 | }); 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lakka [![Build Status](https://travis-ci.org/martinkr/Lakka.svg?branch=master)](https://travis-ci.org/martinkr/Lakka) 2 | lakka. An asynchronous request accelerator. 3 | 4 | lakka is an accelerator for asyncronous requests. It works by caching the request's response in your users local storage. It caches JSON, text and HTML requests and considers the ```Cache-Control``` and ```Expires``` Headers of your requests and responses. 5 | Usualy, the content of an AJAX-request does not change so often. In the best case, the backend-environemnt either caches them or responds with a "not-modified"-like header. But the client still need to wait for this response and the network roundtrip + response time + connection speed makes your website feels "slow". This is where ```lakka``` enhances your website's snappiness: no more waiting, instant responses! 6 | 7 | ## Installation 8 | 9 | Install from npm ```$ yarn add lakka``` 10 | 11 | ## Quick start 12 | 13 | Add ```dist/wrapper/lakka.js``` to your project. 14 | 15 | ## Core Concept 16 | 17 | Basically you need to call two functions: 18 | - ```before()``` with the uri before making your request 19 | - ```after()``` with the response afterwards 20 | 21 | #### .before() 22 | 23 | Call this function before the actual request. It checks the cache and returns a fresh cache item or throws an error. 24 | 25 | #### .after() 26 | 27 | Call this after the actual request. Checks the response and store it at the cache. Returns the cache item or throws an error. 28 | 29 | ## UMD-Module: ```lakka``` 30 | 31 | This is an UMD-Module working in your browser, server, requirejs, browserify, commonjs or nodejs setup. 32 | Load ```./dist/lakka.js``` to use the lakka accelerator in your code. This will give you access to the complete lakka-API. It exposes ```window.lakka```for your convienence. 33 | 34 | ## Examples 35 | 36 | Take a look ```example``` folder: 37 | 38 | #### ```example/xmlhttprequest.js``` 39 | 40 | For a simple example how to use ```lakka``` with ```window.XMLHTTPRequest()``` 41 | 42 | #### ```example/fetch.js``` 43 | 44 | For a simple example how to use ```lakka``` with ```window.fetch()``` 45 | 46 | ## Configuration 47 | 48 | Lakka let's you define basic options: 49 | 50 | - ```exclude```: Adds a regular-expressions to ignore certain URIs. This can be part of the configuration object or directly set using ``.ignore()```. If an URI matches an ```exclude``` and ```include```pattern, the ```exclude```pattern takes precedence and the URI will be ignored. Default: empty 51 | - ```include```: Adds a regular-expressions to include certain URIs. This can be part of the configuration object or directly set using ``.recognize()```. If this is set, all non-matching URIs will be ignored. Default: empty 52 | - ```minutes```: sets the time a cache item is considered "fresh" if the response does not contain a ```Cache-Control Header``` or ```Expires Header```. Default: 60 minutes 53 | 54 | ## API 55 | 56 | ### ```ignore(String|RegEx)``` 57 | 58 | Adds an ignore pattern (a string / RegEx) to the existing / default configuration. URIs matching this pattern will be ignored by lakka and passed throught to the remote location. 59 | 60 | #### Arguments 61 | 62 | - *String|RegEx*: The pattern to look for 63 | 64 | ### ```recognize(String|RegEx)``` 65 | 66 | Adds an include pattern (a string / RegEx) to the existing / default configuration. URIs matching this pattern will be recognized and handled by lakka. If none is set, all URIs will be handled. 67 | 68 | #### Arguments 69 | 70 | - *String|RegEx*: The pattern to look for 71 | 72 | ### ```time(Number)``` 73 | 74 | Sets the minutes (Number) we should cache items for. Defaut value is 60 minutes. It will be overwritten by the ```max-age``` - value of the response's ```Cache-Control Header``` or the response's ```Expires Header```. 75 | 76 | #### Arguments 77 | 78 | - *Number*: The value the default caching time 79 | 80 | ### ```configuration(Object)``` 81 | 82 | Dynamically merge a configuration object into the current / default configuration object. ```include``` and ```exclude``` will be merged, ```minutes``` will be replaced. 83 | 84 | #### Arguments 85 | 86 | - *Object*: A configuration object 87 | 88 | ```JavaScript 89 | { 90 | "include":["/include-me/"], 91 | "exclude":["/exclude-me/"], 92 | "minutes": 120 93 | } 94 | ``` 95 | 96 | ### ```after()``` 97 | Call this after the actual request. Checks the response and store it at the cache. Returns the cache item or an error. 98 | 99 | * Checks the ```status code``` for success or throws an error. 100 | * Checks the ```include```and ```exclude```patterns to see if this should be handled by lakka or throws an error. 101 | * Checks for a cachable ```Cache-Control Header``` or throws an error. 102 | - Cachable: ```public```, ```private```, ```Immutable```, ```proxy-revalidate``` 103 | - Non-cachable ```must-revalidate```, ```no-cache```, ```no-store```, 104 | * Checks the "Content-Type" or throws an error. 105 | - Valid content types are: ```application/json```, ```text/x-json```, ```text/plain```, ```text/html``` 106 | * Checks the ```Expires Header``` and the ```Cache-Control Header``` to see if the content is not already stale or throws an error. 107 | * Creates and saves the cache item 108 | * Returns the cache item or the thrown error 109 | #### Arguments 110 | 111 | #### Returns 112 | Returns the cache item or an error if there's no cache-item for this request. 113 | 114 | ### ```before()``` 115 | Call this before the actual request. Checks for and returns a fresh cache item or an error. 116 | 117 | * Checks the ```include```and ```exclude```patterns to see if this should be handled by lakka or throws an error. 118 | * Checks the ```Cache-Control Header``` or throws an Error 119 | - Cachable: ```public```, ```private```, ```Immutable```, ```proxy-revalidate``` 120 | - Non-cachable ```must-revalidate```, ```no-cache```, ```no-store```, 121 | * Checks the ```Accept Header``` or throws an Error 122 | - Valid content types are: ```application/json```, ```text/x-json```, ```text/plain```, ```text/html`` 123 | * Checks the ```Content Type Header``` or throws an Error 124 | - Valid content types are: ```application/json```, ```text/x-json```, ```text/plain```, ```text/html``` 125 | * Checks there's a fresh item for this URI or throws an Error. 126 | * Returns the cache item or an error if there's no cache-item for this request. 127 | 128 | #### Arguments 129 | 130 | #### Returns 131 | Returns a fresh cache item or an error if there's no cache-item for this request. 132 | 133 | ### ```remove(String)``` 134 | 135 | Force clear an item from lakka. Removes all stored content for this URI. 136 | 137 | #### Arguments 138 | 139 | - *String*: The URI to clear from lakka 140 | 141 | ### ```flush()``` 142 | 143 | Flush the lakka cache. Remove all items. 144 | 145 | 146 | 147 | # Tech Stack 148 | - Writen in ECMAScript 2018 on ```nodejs v9.0.0`` 149 | - Transpiled```babel v6.26.0``` 150 | - Bundled with ```webpack v3.10.0``` 151 | - 100% code coverage using ```mocha v3.5.0```, ```chai v4.1.1``` and ```nyc v11.2.1``` 152 | 153 | 154 | ## License 155 | Licensed under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 156 | 157 | Copyright (c) 2016 - 2021 Martin Krause [http://martinkr.github.io)](http://martinkr.github.io) 158 | -------------------------------------------------------------------------------- /test/cache/calculate-validity.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for calculatingthe validtiy item of a cache item 3 | * 4 | * Calculates the timestamp until this cache entry is valid 5 | * Looks at the cache-control Header, the expires header and finally the default value. 6 | * Returns a timestamp in ms. 7 | * 8 | * Should Calculate the expires valure from "max-age=" and "Expires" header. 9 | * If both Expires and max-age are set max-age will take precedence 10 | 11 | * 12 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 13 | * @license MIT license: https://opensource.org/licenses/MIT 14 | * 15 | * @author Martin Krause 16 | */ 17 | 18 | /* eslint-env amd, mocha */ 19 | 20 | import thisModule from "./../../app/cache/calculate-validity"; 21 | const thisModulePath = "cache/calculate-validity"; 22 | 23 | const oneHour = 1000 * 60 * 60; 24 | let cacheControlHeader; 25 | let expiresHeader; 26 | let defaultValidity; 27 | 28 | 29 | const inTwoHours = () => { 30 | let now = new Date(); 31 | return new Date(now.getTime() + oneHour + oneHour).getTime(); 32 | }; 33 | 34 | describe(`The module "${thisModulePath}"`, () => { 35 | 36 | afterEach((done) => { 37 | done(); 38 | }); 39 | 40 | beforeEach((done) => { 41 | cacheControlHeader = "public, max-age=" + (60 * 60 * 60 * 60) + ", must-validate" // one hour 42 | expiresHeader = "" + new Date(inTwoHours()); 43 | defaultValidity = oneHour; 44 | done(); 45 | }); 46 | 47 | describe("should provide an unified API. It:", () => { 48 | 49 | it("should export a sync function ", () => { 50 | thisModule.should.be.a("function"); 51 | }); 52 | 53 | it("should return the timestamp in ms", () => { 54 | thisModule(cacheControlHeader, expiresHeader, defaultValidity).should.be.a("Number"); 55 | }); 56 | 57 | it("should calculate the value from the Cache-Control HTTP-Header (first argument) if given and max-age is the first item", () => { 58 | const _seed = Math.floor(Math.random() * 999999999999); 59 | const _resultBase = new Date().getTime() + _seed; 60 | cacheControlHeader = "max-age=" + _seed + ", must-validate"; 61 | // max-age contains now + the msseconds for an hour: the validity should be the seeded number plus a bit for runtime 62 | thisModule(cacheControlHeader, expiresHeader, defaultValidity).should.be.within(Number(_resultBase), Number(_resultBase + 100)); 63 | }); 64 | 65 | it("should calculate the value from the Cache-Control HTTP-Header (first argument) if given and max-age is an item", () => { 66 | const _seed = Math.floor(Math.random() * 999999999999); 67 | const _resultBase = new Date().getTime() + _seed; 68 | cacheControlHeader = "public, max-age=" + _seed + ", must-validate"; 69 | // max-age contains now + the msseconds for an hour: the validity should be around one hour (+/- 60sec) 70 | thisModule(cacheControlHeader, expiresHeader, defaultValidity).should.be.within(Number(_resultBase), Number(_resultBase + 100)); 71 | }); 72 | 73 | it("should calculate the value from the Cache-Control HTTP-Header (first argument) if given and max-age is the last item", () => { 74 | const _seed = Math.floor(Math.random() * 999999999999); 75 | const _resultBase = new Date().getTime() + _seed; 76 | cacheControlHeader = "public, max-age=" + _seed; 77 | // max-age contains now + the msseconds for an hour: the validity should be around one hour (+/- 60sec) 78 | thisModule(cacheControlHeader, expiresHeader, defaultValidity).should.be.within(Number(_resultBase), Number(_resultBase + 100)); 79 | }); 80 | 81 | it("should calculate the value from the Expires HTTP-Header (second argument) if the Cache-Control HTTP-Header has no \"max-age\" ", () => { 82 | cacheControlHeader = "public"; 83 | // Expires contains a future timestamp: the validity should be around two hours (+/- 60sec) 84 | thisModule(cacheControlHeader, expiresHeader, defaultValidity).should.be.within(inTwoHours() - (1000 * 60), inTwoHours() + (1000 * 60)); 85 | }); 86 | 87 | it("should calculate the value from the Expires HTTP-Header (second argument) if the Cache-Control HTTP-Header is empty ", () => { 88 | // Expires contains a timestamp in two hours : the validity should be around two hours (+/- 60sec) 89 | thisModule(null, expiresHeader, defaultValidity).should.be.within(inTwoHours() - (1000 * 60), inTwoHours() + (1000 * 60)); 90 | }); 91 | 92 | it("should calculate the value from the Expires HTTP-Header (second argument) if the Cache-Control HTTP-Header is not a number ", () => { 93 | cacheControlHeader = "public, max-age=foo"; 94 | // Expires contains a timestamp in two hours : the validity should be around two hours (+/- 60sec) 95 | thisModule(cacheControlHeader, expiresHeader, defaultValidity).should.be.within(inTwoHours() - (1000 * 60), inTwoHours() + (1000 * 60)); 96 | }); 97 | 98 | it("should calculate the value from the default value if the Expires HTTP-Header is not evaluable (0) and the Cache-Control HTTP-Header is empty", () => { 99 | // expires contains a timestamp not a valid datestring 100 | expiresHeader = "" + "0"; 101 | const _resultBase = new Date().getTime(); 102 | // default is one hour : the validity should be around one hour (+/- 60sec) 103 | thisModule(null, expiresHeader, defaultValidity).should.be.within(_resultBase + oneHour - (1000 * 60), _resultBase + oneHour + (1000 * 60)); 104 | }); 105 | 106 | it("should calculate the value from the default value if the Expires HTTP-Header is not evaluable (1234) and the Cache-Control HTTP-Header is empty", () => { 107 | // expires contains a timestamp not a valid datestring 108 | expiresHeader = "" + "1234"; 109 | const _resultBase = new Date().getTime(); 110 | // default is one hour : the validity should be around one hour (+/- 60sec) 111 | thisModule(null, expiresHeader, defaultValidity).should.be.within(_resultBase + oneHour - (1000 * 60), _resultBase + oneHour + (1000 * 60)); 112 | }); 113 | 114 | it("should calculate the value from the default value if the Expires HTTP-Header is not evaluable (foobar) and the Cache-Control HTTP-Header is empty", () => { 115 | // expires contains a timestamp not a valid datestring 116 | expiresHeader = "" + "foobar"; 117 | const _resultBase = new Date().getTime(); 118 | // default is one hour : the validity should be around one hour (+/- 60sec) 119 | thisModule(null, expiresHeader, defaultValidity).should.be.within(_resultBase + oneHour - (1000 * 60), _resultBase + oneHour + (1000 * 60)); 120 | }); 121 | 122 | it("should calculate the value from the default value if the Expires HTTP-Header (second argument) and the Cache-Control HTTP-Header are empty", () => { 123 | const _resultBase = new Date().getTime(); 124 | // default is one hour : the validity should be around one hour (+/- 60sec) 125 | thisModule(null, null, defaultValidity).should.be.within(_resultBase + oneHour - (1000 * 60), _resultBase + oneHour + (1000 * 60)); 126 | }); 127 | 128 | it("should calculate the value from the default value if the Expires HTTP-Header (second argument)is empty and the Cache-Control HTTP-Header has no max-age", () => { 129 | cacheControlHeader = "public" 130 | const _resultBase = new Date().getTime(); 131 | // default is one hour : the validity should be around one hour (+/- 60sec) 132 | thisModule(cacheControlHeader, null, defaultValidity).should.be.within(_resultBase + oneHour - (1000 * 60), _resultBase + oneHour + (1000 * 60)); 133 | }); 134 | 135 | 136 | }); 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /test/facade/sessionstorage.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for the window.sessionstorage facade 3 | * This module provides a facade for accessing the internal private cache. 4 | * In this case: we're using the sessionStorage 5 | * 6 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 7 | * @license MIT license: https://opensource.org/licenses/MIT 8 | * 9 | * @author Martin Krause 10 | */ 11 | 12 | /* eslint-env mocha */ 13 | 14 | 15 | import thisModule from "./../../app/facade/sessionstorage"; 16 | const thisModulePath = "facade/sessionstorage"; 17 | 18 | let spySet; 19 | let spyGet; 20 | let spyDel; 21 | 22 | 23 | describe(`The module "${thisModulePath}"`, () => { 24 | 25 | afterEach((done) => { 26 | spySet.restore(); 27 | spyGet.restore(); 28 | spyDel.restore(); 29 | done(); 30 | }); 31 | 32 | beforeEach((done) => { 33 | if (!global.window) { 34 | global.window = {}; 35 | } 36 | if (!global.window.sessionStorage) { 37 | global.window.sessionStorage = { 38 | getItem() { 39 | return "stub"; 40 | }, 41 | setItem() { }, 42 | removeItem() { }, 43 | }; 44 | } 45 | spySet = sinon.spy(global.window.sessionStorage, "setItem"); 46 | spyGet = sinon.spy(global.window.sessionStorage, "getItem"); 47 | spyDel = sinon.spy(global.window.sessionStorage, "removeItem"); 48 | done(); 49 | }); 50 | 51 | 52 | after((done) => { 53 | spySet.resetHistory(); 54 | spyGet.resetHistory(); 55 | spyDel.resetHistory(); 56 | done(); 57 | }); 58 | 59 | describe("should provide an unified API. It:", () => { 60 | 61 | it("should export a function \"set\" which is sync", () => { 62 | (Object.getPrototypeOf(thisModule.set).constructor.name === "Function").should.be.ok; 63 | }); 64 | 65 | it("should export a function \"get\" which is sync", () => { 66 | (Object.getPrototypeOf(thisModule.get).constructor.name === "Function").should.be.ok; 67 | }); 68 | 69 | it("should export a function \"has\" which is ", () => { 70 | (Object.getPrototypeOf(thisModule.has).constructor.name === "Function").should.be.ok; 71 | }); 72 | 73 | it("should export a function \"del\" which is ", () => { 74 | (Object.getPrototypeOf(thisModule.del).constructor.name === "Function").should.be.ok; 75 | }); 76 | 77 | }); 78 | 79 | describe("should have an API \"set\". It:", () => { 80 | 81 | it("should call \"window.sessionStorage.setItem\"", (() => { 82 | try { 83 | thisModule.set("foo", "bar"); 84 | } catch (err) { 85 | console.error("error: ", err) 86 | } 87 | spySet.should.have.been.calledWith("foo", "bar"); 88 | return true; 89 | })); 90 | 91 | it("should throw if \"window.sessionStorage.setItem\" is not available", (() => { 92 | spySet.restore(); 93 | window.sessionStorage.setItem = undefined; 94 | 95 | try { 96 | thisModule.set("foo", "bar"); 97 | } catch (err) { 98 | err.should.be.an("error"); 99 | } 100 | return true; 101 | })); 102 | 103 | it("should throw if we're missing both arguments", (() => { 104 | try { 105 | thisModule.set(); 106 | } catch (err) { 107 | err.should.be.an("error"); 108 | return true; 109 | } 110 | throw new Error("Failed"); 111 | })); 112 | 113 | it("should throw if we're missing one arguments", (() => { 114 | try { 115 | thisModule.set("foo"); 116 | } catch (err) { 117 | err.should.be.an("error"); 118 | return true; 119 | } 120 | throw new Error("Failed"); 121 | })); 122 | 123 | }); 124 | 125 | 126 | describe("should have an API \"get\". It:", () => { 127 | 128 | it("should call \"window.sessionStorage.getItem\"", (() => { 129 | try { 130 | thisModule.get("foo"); 131 | } catch (err) { 132 | console.error("error: ", err) 133 | } 134 | spyGet.should.have.been.calledWith("foo"); 135 | return true; 136 | })); 137 | 138 | it("should throw if \"window.sessionStorage.getItem\" is not available", (() => { 139 | spyGet.restore(); 140 | window.sessionStorage.getItem = undefined; 141 | 142 | try { 143 | thisModule.get("foo"); 144 | } catch (err) { 145 | err.should.be.an("error"); 146 | } 147 | return true; 148 | })); 149 | 150 | it("should throw if we're missing the arguments", (() => { 151 | try { 152 | thisModule.get(); 153 | } catch (err) { 154 | err.should.be.an("error"); 155 | return true; 156 | } 157 | throw new Error("Failed"); 158 | })); 159 | 160 | it("should return the value from window.sessionStorage", (() => { 161 | window.sessionStorage.getItem = () => { 162 | return "baz"; 163 | }; 164 | let _result = thisModule.get("foo"); 165 | _result.should.equal("baz"); 166 | return true; 167 | })); 168 | 169 | it("should return the value from window.sessionStorage - \"null\" if there's no value for the key", (() => { 170 | window.sessionStorage.getItem = () => { 171 | return null; 172 | }; 173 | let _result = thisModule.get("foo"); 174 | (_result === null).should.be.true; 175 | return true; 176 | })); 177 | 178 | }); 179 | 180 | 181 | describe("should have an API \"del\". It:", () => { 182 | 183 | it("should call \"window.sessionStorage.removeItem\"", (() => { 184 | try { 185 | thisModule.del("foo"); 186 | } catch (err) { 187 | console.error("error: ", err) 188 | } 189 | spyDel.should.have.been.calledWith("foo"); 190 | return true; 191 | })); 192 | 193 | it("should throw if \"window.sessionStorage.removeItem\" is not available", (() => { 194 | spyDel.restore(); 195 | window.sessionStorage.removeItem = undefined; 196 | 197 | try { 198 | thisModule.del("foo"); 199 | } catch (err) { 200 | err.should.be.an("error"); 201 | } 202 | return true; 203 | })); 204 | 205 | 206 | it("should throw if we're missing the arguments", (() => { 207 | try { 208 | thisModule.del(); 209 | } catch (err) { 210 | err.should.be.an("error"); 211 | return true; 212 | } 213 | throw new Error("Failed"); 214 | })); 215 | 216 | }); 217 | 218 | describe("should have an API \"has\". It:", () => { 219 | 220 | it("should call \"window.sessionStorage.getItem\"", (() => { 221 | try { 222 | thisModule.has("foo"); 223 | } catch (err) { 224 | console.error("error: ", err) 225 | } 226 | spyGet.should.have.been.calledWith("foo"); 227 | return true; 228 | })); 229 | 230 | it("should throw if \"window.sessionStorage.getItem\" is not available", (() => { 231 | spyGet.restore(); 232 | window.sessionStorage.getItem = undefined; 233 | 234 | try { 235 | thisModule.has("foo"); 236 | } catch (err) { 237 | err.should.be.an("error"); 238 | } 239 | return true; 240 | })); 241 | 242 | it("should throw if we're missing the arguments", (() => { 243 | try { 244 | thisModule.has(); 245 | } catch (err) { 246 | err.should.be.an("error"); 247 | return true; 248 | } 249 | throw new Error("Failed"); 250 | })); 251 | 252 | it("should return true if there's an entry for the key", (() => { 253 | window.sessionStorage.getItem = () => { 254 | return "baz"; 255 | }; 256 | let _result = thisModule.has("foo"); 257 | _result.should.be.ok; 258 | return true; 259 | })); 260 | 261 | it("should return false if there's no entry for the key", (() => { 262 | window.sessionStorage.getItem = () => { 263 | return null; 264 | }; 265 | let _result = thisModule.has("foo"); 266 | _result.should.not.be.ok; 267 | return true; 268 | })); 269 | 270 | }); 271 | 272 | }); 273 | -------------------------------------------------------------------------------- /test/cache/create-item.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for creating the cache item 3 | * Each item has: 4 | * 5 | * "key": create-key(uri) 6 | * "until": freshness([default, "cache-control", "expires"]) 7 | * "headers" : 8 | * "X-Status-Code": 200 9 | * "Cache-Control": 10 | * "Expires": 11 | * "Content-Type": application-type 12 | * "Status": "200 from cache" 13 | * "responseText": string 14 | * 15 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 16 | * @license MIT license: https://opensource.org/licenses/MIT 17 | * 18 | * @author Martin Krause 19 | */ 20 | 21 | /* eslint-env mocha */ 22 | 23 | 24 | import createKey from "./../../app/cache/create-key"; 25 | import thisModule from "./../../app/cache/create-item"; 26 | const thisModulePath = "cache/create-item"; 27 | 28 | const uri = "protocol://path/to/my/resouce"; 29 | const responseString = "content"; 30 | const key = createKey(uri); 31 | const headers = { 32 | "Content-Type": "text/plain" 33 | } 34 | 35 | const _resultBase = new Date().getTime(); 36 | const oneHour = 1000 * 60 * 60; 37 | 38 | describe(`The module "${thisModulePath}"`, () => { 39 | 40 | afterEach((done) => { 41 | done(); 42 | }); 43 | 44 | beforeEach((done) => { 45 | done(); 46 | }); 47 | 48 | describe("should provide an unified API. It:", () => { 49 | 50 | it("should export a sync function ", () => { 51 | thisModule.should.be.a("function"); 52 | }); 53 | 54 | it("should throw an error if the first argument is not a string", () => { 55 | try { 56 | thisModule(true, responseString, headers); 57 | } catch (err) { 58 | err.should.be.an("error"); 59 | return true; 60 | } 61 | throw new Error("Failed"); 62 | }); 63 | 64 | it("should throw an error if the second argument is not a string", () => { 65 | try { 66 | thisModule(uri, true, headers); 67 | } catch (err) { 68 | err.should.be.an("error"); 69 | return true; 70 | } 71 | throw new Error("Failed"); 72 | }); 73 | 74 | it("should throw an error if the third argument is not an Object", () => { 75 | try { 76 | thisModule(uri, responseString, true); 77 | } catch (err) { 78 | err.should.be.an("error"); 79 | return true; 80 | } 81 | throw new Error("Failed"); 82 | }); 83 | 84 | it("should not throw an error if the optional third argument is missing", () => { 85 | try { 86 | thisModule(uri, responseString); 87 | } catch (err) { 88 | throw new Error("Failed"); 89 | } 90 | return true; 91 | }); 92 | 93 | }); 94 | 95 | describe("should create the cache item. It:", () => { 96 | 97 | it("should create an item from two arguments", () => { 98 | thisModule(uri, responseString).should.be.an("Object"); 99 | }); 100 | 101 | it("should create an item from three arguments", () => { 102 | thisModule(uri, responseString, headers).should.be.an("Object"); 103 | }); 104 | 105 | it("should create an item wich has a valid \"key\"", () => { 106 | thisModule(uri, responseString, headers)["key"].should.equal(key); 107 | }); 108 | 109 | it("should create an item wich has a valid \"status\"", () => { 110 | thisModule(uri, responseString, headers)["status"].should.equal(200); 111 | }); 112 | 113 | it("should create an item wich has a valid \"statusText\"", () => { 114 | thisModule(uri, responseString, headers)["statusText"].should.equal("cache"); 115 | }); 116 | 117 | it("should create an item wich has a valid \"responseText\"", () => { 118 | thisModule(uri, responseString, headers)["responseText"].should.equal(responseString); 119 | }); 120 | 121 | it("should create an item wich has a valid \"until\"", () => { 122 | thisModule(uri, responseString, headers)["until"].should.be.a("Number"); 123 | }); 124 | 125 | it("should create an item wich has a valid \"type\" from \"headers\"", () => { 126 | thisModule(uri, responseString, headers)["headers"]["Content-Type"].should.equal(headers["Content-Type"]); 127 | }); 128 | 129 | it("should create an item wich has a valid \"headers.Content-Type\" from \"headers\"", () => { 130 | thisModule(uri, responseString, headers)["headers"]["Content-Type"].should.equal(headers["Content-Type"]); 131 | }); 132 | 133 | it("should create an item wich has a valid \"headers.Status\": \"200 cache\"", () => { 134 | thisModule(uri, responseString, headers)["headers"]["Status"].should.equal("200 cache"); 135 | }); 136 | 137 | it("should create an item wich has a valid \"headers.Expires\" from \"headers.Expires\" ", () => { 138 | headers["Expires"] = (new Date(new Date(new Date().getTime() + oneHour + oneHour).getTime())); 139 | thisModule(uri, responseString, headers)["headers"]["Expires"].should.equal(headers["Expires"]); 140 | }); 141 | 142 | it("should create an item wich has a no \"headers.Expires\" if there's no \"headers.Expires\" ", () => { 143 | headers["Expires"] = null; 144 | thisModule(uri, responseString, headers)["headers"].should.not.have.property("Expires"); 145 | }); 146 | 147 | it("should create an item wich has a valid \"headers.Cache-Control\" from \"headers.Cache-Control\" ", () => { 148 | headers["Cache-Control"] = ("public, max-age=" + (Math.floor(Math.random() * 999999999999))); 149 | thisModule(uri, responseString, headers)["headers"]["Cache-Control"].should.equal(headers["Cache-Control"]); 150 | }); 151 | 152 | it("should create an item wich has a no \"headers.Cache-Control\" if there's no \"headers.Cache-Control\" ", () => { 153 | headers["Cache-Control"] = null; 154 | thisModule(uri, responseString, headers)["headers"].should.not.have.property("Cache-Control"); 155 | }); 156 | 157 | }); 158 | 159 | describe("should work with missing headers for \"Content-Type\". It:", () => { 160 | it("should use the default \"text/plain\" for \"type\"", () => { 161 | thisModule(uri, responseString).headers["Content-Type"].should.equal("text/plain"); 162 | }); 163 | 164 | it("should use the lowercase \"content-type\" for \"Content-Type\"", () => { 165 | thisModule(uri, responseString, { "content-type": "x-custom" }).headers["Content-Type"].should.equal("x-custom"); 166 | }); 167 | }); 168 | 169 | describe("should work with missing headers for \"Validity\". It:", () => { 170 | 171 | it("should use the \"Cache-Control\" for \"until\"", () => { 172 | const _seed = Math.floor(Math.random() * 999999999999); 173 | const _resultBase = new Date().getTime() + _seed; 174 | // Cache-Control header is two hours : the validity should be around two hours (+/- 60sec) 175 | thisModule(uri, responseString, { "Cache-Control": ("public, max-age=" + _seed) })["until"].should.be.within(Number(_resultBase), Number(_resultBase + 100)); 176 | }); 177 | 178 | it("should use the lowercase \"cache-control\" for \"until\"", () => { 179 | const _seed = Math.floor(Math.random() * 999999999999); 180 | const _resultBase = new Date().getTime() + _seed; 181 | // Cache-Control header is two hours : the validity should be around two hours (+/- 60sec) 182 | thisModule(uri, responseString, { "cache-control": ("public, max-age=" + _seed) })["until"].should.be.within(Number(_resultBase), Number(_resultBase + 100)); 183 | }); 184 | 185 | it("should use the \"Expires\" for \"until\"", () => { 186 | // Expires header is two hours : the validity should be around two hours (+/- 60sec) 187 | thisModule(uri, responseString, { "Expires": (new Date(new Date(new Date().getTime() + oneHour + oneHour).getTime())) })["until"].should.be.within(_resultBase + oneHour + oneHour - (1000 * 60), _resultBase + oneHour + oneHour + (1000 * 60)); 188 | }); 189 | 190 | it("should use the lowercase \"expires\" for \"until\"", () => { 191 | // Expires header is two hours : the validity should be around two hours (+/- 60sec) 192 | thisModule(uri, responseString, { "expires": (new Date(new Date(new Date().getTime() + oneHour + oneHour).getTime())) })["until"].should.be.within(_resultBase + oneHour + oneHour - (1000 * 60), _resultBase + oneHour + oneHour + (1000 * 60)); 193 | }); 194 | 195 | it("should use the default \"60 minutes\" for \"until\"", () => { 196 | // default is one hour : the validity should be around one hour (+/- 60sec) 197 | thisModule(uri, responseString)["until"].should.be.within(_resultBase + oneHour - (1000 * 60), _resultBase + oneHour + (1000 * 60)); 198 | }); 199 | 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/configuration/main.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3 | * Specs for the configuration functionality 4 | * This module provides the funcitonality to configure the lakka-cache. 5 | * @exports get () 6 | * @exports set () 7 | * 8 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 9 | * @license MIT license: https://opensource.org/licenses/MIT 10 | * 11 | * @author Martin Krause 12 | */ 13 | 14 | /* eslint-env mocha */ 15 | 16 | import thisModule from "./../../app/configuration/main"; 17 | const thisModulePath = "configuration/main"; 18 | 19 | describe(`The module "${thisModulePath}"`, () => { 20 | 21 | afterEach((done) => { 22 | done(); 23 | }); 24 | 25 | beforeEach(() => { 26 | let _object = { 27 | "include": [], 28 | "exclude": [], 29 | "minutes": 60 30 | }; 31 | thisModule.set(_object); 32 | 33 | }); 34 | 35 | 36 | describe("should provide an API. It:", () => { 37 | 38 | it("should export a function \"set\" which is sync", () => { 39 | (Object.getPrototypeOf(thisModule.set).constructor.name === "Function").should.be.ok; 40 | }); 41 | 42 | it("should export a function \"get\" which is sync", () => { 43 | (Object.getPrototypeOf(thisModule.get).constructor.name === "Function").should.be.ok; 44 | }); 45 | 46 | }); 47 | 48 | 49 | describe("should have a default configuration. It:", () => { 50 | 51 | it("should cache items for 60minutes", (() => { 52 | let _result = thisModule.get("minutes"); 53 | _result.should.equal(60); 54 | })); 55 | 56 | it("should not has an include pattern set", (() => { 57 | let _result = thisModule.get("include"); 58 | _result.should.deep.equal([]); 59 | })); 60 | 61 | it("should not has an exclude pattern set", (() => { 62 | let _result = thisModule.get("exclude"); 63 | _result.should.deep.equal([]); 64 | })); 65 | 66 | }); 67 | 68 | describe("should have an API \"set\". It:", () => { 69 | 70 | it("should throw if we're missing one string arguments", (() => { 71 | try { 72 | thisModule.set("foo"); 73 | } catch (err) { 74 | err.should.be.an("error"); 75 | return true; 76 | } 77 | throw new Error("Failed"); 78 | })); 79 | 80 | it("should throw if we're missing all arguments", (() => { 81 | try { 82 | thisModule.set(); 83 | } catch (err) { 84 | err.should.be.an("error"); 85 | return true; 86 | } 87 | throw new Error("Failed"); 88 | })); 89 | 90 | it("should take a configuration object and store it", (() => { 91 | let _object = { 92 | "include": ["include/*"], 93 | "exclude": ["exclude/*"], 94 | "minutes": 200 95 | }; 96 | thisModule.set(_object); 97 | let _result = thisModule.get("minutes"); 98 | _result.should.equal(_object.minutes); 99 | })); 100 | 101 | it("should take a partial configuration object and merge it", (() => { 102 | let _object = { 103 | "include": ["include/*"], 104 | "minutes": 200 105 | }; 106 | 107 | thisModule.set(_object); 108 | let _result = thisModule.get("minutes"); 109 | 110 | _result.should.equal(_object.minutes); 111 | _result = thisModule.get("include"); 112 | _result.should.equal(_object.include); 113 | 114 | // exclude should be the unmodified default 115 | _result = thisModule.get("exclude"); 116 | _result.should.deep.equal([]); 117 | })); 118 | 119 | it("should throw if the object has invalid properties", (() => { 120 | try { 121 | thisModule.set({ 122 | "foo": "bar" 123 | }); 124 | } catch (err) { 125 | err.should.be.an("error"); 126 | return true; 127 | } 128 | throw new Error("Failed"); 129 | })); 130 | 131 | it("should take a configuration key-value pair and store the value", (() => { 132 | thisModule.set("minutes", 100); 133 | thisModule.set("minutes", 200); 134 | let _result = thisModule.get("minutes"); 135 | _result.should.deep.equal(200); 136 | })); 137 | 138 | it("should throw if the key-value pair's key is invalid", (() => { 139 | try { 140 | thisModule.set("foo", "bar"); 141 | } catch (err) { 142 | err.should.be.an("error"); 143 | return true; 144 | } 145 | throw new Error("Failed"); 146 | })); 147 | 148 | it("should throw if the key or a pair is not a string", (() => { 149 | try { 150 | thisModule.set(true, "value"); 151 | } catch (err) { 152 | err.should.be.an("error"); 153 | return true; 154 | } 155 | throw new Error("Failed"); 156 | })); 157 | 158 | it("should throw if the value is an Object", (() => { 159 | try { 160 | thisModule.set("inlcude", { "foo": "bar" }); 161 | } catch (err) { 162 | err.should.be.an("error"); 163 | return true; 164 | } 165 | throw new Error("Failed"); 166 | })); 167 | 168 | it("should throw if the value is an Array", (() => { 169 | try { 170 | thisModule.set("include", ["string"]); 171 | } catch (err) { 172 | err.should.be.an("error"); 173 | return true; 174 | } 175 | throw new Error("Failed"); 176 | })); 177 | 178 | it("should throw if the value is an Object", (() => { 179 | try { 180 | thisModule.set("include", { "foo": "bar" }); 181 | } catch (err) { 182 | err.should.be.an("error"); 183 | return true; 184 | } 185 | throw new Error("Failed"); 186 | })); 187 | 188 | it("should throw if the value is a Function", (() => { 189 | try { 190 | thisModule.set("include", () => false); 191 | } catch (err) { 192 | err.should.be.an("error"); 193 | return true; 194 | } 195 | throw new Error("Failed"); 196 | })); 197 | 198 | it("should not throw if the value for \"minutes\" is a number ", (() => { 199 | let _result = thisModule.set("minutes", 999); 200 | _result.should.be.true; 201 | })); 202 | 203 | it("should throw if the value for \"minutes\" is not a parsable to a number ", (() => { 204 | try { 205 | thisModule.set("minutes", "bar"); 206 | } catch (err) { 207 | err.should.be.an("error"); 208 | return true; 209 | } 210 | throw new Error("Failed"); 211 | })); 212 | 213 | it("should throw if the value for \"minutes\" is a boolean", (() => { 214 | try { 215 | thisModule.set("minutes", true); 216 | } catch (err) { 217 | err.should.be.an("error"); 218 | return true; 219 | } 220 | throw new Error("Failed"); 221 | })); 222 | 223 | it("should throw if the value for \"minutes\" is an Array", (() => { 224 | try { 225 | thisModule.set("minutes", [1]); 226 | } catch (err) { 227 | err.should.be.an("error"); 228 | return true; 229 | } 230 | throw new Error("Failed"); 231 | })); 232 | 233 | it("should throw if the value for \"minutes\" is an Object", (() => { 234 | try { 235 | thisModule.set("minutes", { "foo": "bar" }); 236 | } catch (err) { 237 | err.should.be.an("error"); 238 | return true; 239 | } 240 | throw new Error("Failed"); 241 | })); 242 | 243 | it("should throw if the value for \"minutes\" is a Function", (() => { 244 | try { 245 | thisModule.set("minutes", () => false); 246 | } catch (err) { 247 | err.should.be.an("error"); 248 | return true; 249 | } 250 | throw new Error("Failed"); 251 | })); 252 | 253 | it("should not set a value twice for \"include\"", (() => { 254 | thisModule.set("include", ".?"); 255 | thisModule.set("include", ".?"); 256 | let _result = thisModule.get("include"); 257 | _result.should.have.a.lengthOf(1); 258 | })); 259 | 260 | it("should not set a value twice for \"exclude\"", (() => { 261 | thisModule.set("exclude", ".?"); 262 | thisModule.set("exclude", ".?"); 263 | let _result = thisModule.get("exclude"); 264 | _result.should.have.a.lengthOf(1); 265 | })); 266 | 267 | it("should convert the value for \"include\" to a regexp", (() => { 268 | thisModule.set("include", ".?"); 269 | let _result = thisModule.get("include"); 270 | _result[0].should.be.a("RegExp"); 271 | })); 272 | 273 | it("should convert the value for \"exclude\" to a regexp", (() => { 274 | thisModule.set("exclude", ".?"); 275 | let _result = thisModule.get("exclude"); 276 | _result[0].should.be.a("RegExp"); 277 | })); 278 | 279 | }); 280 | 281 | describe("should have an API \"get\". It:", () => { 282 | 283 | 284 | it("should take one string argument", (() => { 285 | thisModule.get("minutes"); 286 | (true).should.be.ok; 287 | })); 288 | 289 | it("should throw if we're missing all arguments", (() => { 290 | try { 291 | thisModule.get(); 292 | } catch (err) { 293 | err.should.be.an("error"); 294 | return true; 295 | } 296 | throw new Error("Failed"); 297 | })); 298 | 299 | it("should throw if the key is a boolean", (() => { 300 | try { 301 | thisModule.get(true); 302 | } catch (err) { 303 | err.should.be.an("error"); 304 | return true; 305 | } 306 | throw new Error("Failed"); 307 | })); 308 | 309 | it("should throw if the key is an Array", (() => { 310 | try { 311 | thisModule.get([1]); 312 | } catch (err) { 313 | err.should.be.an("error"); 314 | return true; 315 | } 316 | throw new Error("Failed"); 317 | })); 318 | 319 | it("should throw if the key is an Object", (() => { 320 | try { 321 | thisModule.get({ "foo": "bar" }); 322 | } catch (err) { 323 | err.should.be.an("error"); 324 | return true; 325 | } 326 | throw new Error("Failed"); 327 | })); 328 | 329 | it("should throw if the key is not valid", (() => { 330 | try { 331 | thisModule.get("foo"); 332 | } catch (err) { 333 | err.should.be.an("error"); 334 | return true; 335 | } 336 | throw new Error("Failed"); 337 | })); 338 | 339 | it("should return a number for \"minutes\"", (() => { 340 | let _result = thisModule.get("minutes"); 341 | _result.should.be.a("number"); 342 | })); 343 | 344 | it("should return an array for \"include\"", (() => { 345 | let _result = thisModule.get("include"); 346 | _result.should.be.an("array"); 347 | })); 348 | 349 | it("should return an array with regexp for \"include\"", (() => { 350 | thisModule.set("include", ".?"); 351 | let _result = thisModule.get("include"); 352 | _result[0].should.be.a("RegExp"); 353 | })); 354 | 355 | it("should return an array for \"exclude\"", (() => { 356 | let _result = thisModule.get("exclude"); 357 | _result.should.be.an("array"); 358 | })); 359 | 360 | it("should return an array with regexp for \"exclude\"", (() => { 361 | thisModule.set("exclude", ".?"); 362 | let _result = thisModule.get("exclude"); 363 | _result[0].should.be.a("RegExp"); 364 | })); 365 | 366 | 367 | }); 368 | 369 | 370 | 371 | 372 | }); 373 | -------------------------------------------------------------------------------- /test/facade/localstorage.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for the window.localstorage facade 3 | * This module provides a facade for accessing the internal private cache. 4 | * In this case: we're using the localStorage 5 | * 6 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 7 | * @license MIT license: https://opensource.org/licenses/MIT 8 | * 9 | * @author Martin Krause 10 | */ 11 | 12 | /* eslint-env mocha */ 13 | 14 | import thisModule from "./../../app/facade/localstorage"; 15 | const thisModulePath = "facade/localstorage"; 16 | 17 | let spySet; 18 | let spyGet; 19 | let spyDel; 20 | let spyFlush; 21 | 22 | 23 | describe(`The module "${thisModulePath}"`, () => { 24 | 25 | afterEach((done) => { 26 | global.window.localStorage.clear(); 27 | spySet.restore(); 28 | spyGet.restore(); 29 | spyDel.restore(); 30 | spyFlush.restore(); 31 | done(); 32 | }); 33 | 34 | 35 | before((done) => { 36 | done(); 37 | }); 38 | beforeEach((done) => { 39 | spySet = sinon.spy(global.window.localStorage, "setItem"); 40 | spyGet = sinon.spy(global.window.localStorage, "getItem"); 41 | spyDel = sinon.spy(global.window.localStorage, "removeItem"); 42 | spyFlush = sinon.spy(global.window.localStorage, "clear"); 43 | done(); 44 | }); 45 | 46 | after((done) => { 47 | spySet.resetHistory(); 48 | spyGet.resetHistory(); 49 | spyDel.resetHistory(); 50 | spyFlush.resetHistory(); 51 | done(); 52 | }); 53 | 54 | 55 | describe("should provide an unified API. It:", () => { 56 | 57 | it("should export a function \"set\" which is sync", () => { 58 | (Object.getPrototypeOf(thisModule.set).constructor.name === "Function").should.be.ok; 59 | }); 60 | 61 | it("should export a function \"get\" which is sync", () => { 62 | (Object.getPrototypeOf(thisModule.get).constructor.name === "Function").should.be.ok; 63 | }); 64 | 65 | it("should export a function \"del\" which is sync", () => { 66 | (Object.getPrototypeOf(thisModule.del).constructor.name === "Function").should.be.ok; 67 | }); 68 | 69 | }); 70 | 71 | 72 | 73 | describe("should have an API \"set\". It:", () => { 74 | 75 | it("should return true if everything is fine", (() => { 76 | let _result; 77 | try { 78 | _result = thisModule.set("foo", "bar"); 79 | } catch (err) { 80 | console.error("error: ", err) 81 | } 82 | _result.should.be.ok; 83 | return true; 84 | })); 85 | 86 | it("should call \"window.localStorage.setItem\" and save the values unter \"lakka\"", (() => { 87 | try { 88 | thisModule.set("foo", "bar"); 89 | } catch (err) { 90 | console.error("error: ", err) 91 | } 92 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 93 | "foo": "bar" 94 | })); 95 | return true; 96 | })); 97 | 98 | 99 | it("should call \"window.localStorage.setItem\" and save the values unter \"lakka\" even if there are already values", (() => { 100 | try { 101 | thisModule.set("foo", "foofoo"); 102 | thisModule.set("bar", "barfoo"); 103 | thisModule.set("baz", "bazfoo"); 104 | } catch (err) { 105 | console.error("error: ", err) 106 | } 107 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 108 | "foo": "foofoo", 109 | "bar": "barfoo", 110 | "baz": "bazfoo" 111 | })); 112 | return true; 113 | })); 114 | 115 | 116 | it("should save Strings only (stringify an Object)", (() => { 117 | try { 118 | thisModule.set("foo", { 119 | "bar": "baz" 120 | }); 121 | } catch (err) { 122 | console.error("error: ", err) 123 | } 124 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 125 | "foo": { 126 | "bar": "baz" 127 | } 128 | })); 129 | return true; 130 | })); 131 | 132 | it("should save Strings only (stringify an Array)", (() => { 133 | try { 134 | thisModule.set("foo", ["bar", "baz"]); 135 | } catch (err) { 136 | console.error("error: ", err) 137 | } 138 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 139 | "foo": ["bar", "baz"] 140 | })); 141 | return true; 142 | })); 143 | 144 | it("should save Strings only (stringify a Number)", (() => { 145 | try { 146 | thisModule.set("foo", 10); 147 | } catch (err) { 148 | console.error("error: ", err) 149 | } 150 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 151 | "foo": 10 152 | })); 153 | return true; 154 | })); 155 | 156 | it("should save Strings only (stringify a Boolean true)", (() => { 157 | try { 158 | thisModule.set("foo", true); 159 | } catch (err) { 160 | console.error("error: ", err) 161 | } 162 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 163 | "foo": true 164 | })); 165 | return true; 166 | })); 167 | 168 | it("should throw if the value evaluates to false (false)", (() => { 169 | try { 170 | thisModule.set("foo", false); 171 | } catch (err) { 172 | err.should.be.an("error"); 173 | return true; 174 | } 175 | throw new Error("Failed"); 176 | })); 177 | 178 | 179 | it("should create a fresh \"lakka\"-object if the storage is blank ", (() => { 180 | global.window.localStorage.clear(); 181 | try { 182 | thisModule.set("foo", "bar"); 183 | } catch (err) { 184 | console.error("error: ", err) 185 | } 186 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 187 | "foo": "bar" 188 | })); 189 | })); 190 | 191 | 192 | it("should throw if the value evaluates to false (0)", (() => { 193 | try { 194 | thisModule.set("foo", 0); 195 | } catch (err) { 196 | err.should.be.an("error"); 197 | return true; 198 | } 199 | throw new Error("Failed"); 200 | })); 201 | 202 | it("should not throw if the value is an empty string (\"\")", (() => { 203 | try { 204 | thisModule.set("foo", ""); 205 | } catch (err) { 206 | console.error("error: ", err) 207 | } 208 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 209 | "foo": "" 210 | })); 211 | return true; 212 | })); 213 | 214 | 215 | it("should throw if \"window.localStorage.setItem\" is not available", (() => { 216 | spySet.resetHistory(); 217 | window.localStorage.setItem = undefined; 218 | 219 | try { 220 | thisModule.set("foo", "bar"); 221 | } catch (err) { 222 | err.should.be.an("error"); 223 | } 224 | return true; 225 | })); 226 | 227 | it("should throw if we're missing both arguments", (() => { 228 | try { 229 | thisModule.set(); 230 | } catch (err) { 231 | err.should.be.an("error"); 232 | return true; 233 | } 234 | throw new Error("Failed"); 235 | })); 236 | 237 | it("should throw if we're missing one arguments", (() => { 238 | try { 239 | thisModule.set("foo"); 240 | } catch (err) { 241 | err.should.be.an("error"); 242 | return true; 243 | } 244 | throw new Error("Failed"); 245 | })); 246 | 247 | 248 | 249 | }); 250 | 251 | 252 | describe("should have an API \"get\". It:", () => { 253 | 254 | it("should call \"window.localStorage.getItem\" and get the \"lakka\" object", (() => { 255 | try { 256 | thisModule.get("foo"); 257 | } catch (err) { 258 | // console.error("error: ", err) 259 | } 260 | spyGet.should.have.been.calledWith("lakka"); 261 | return true; 262 | })); 263 | 264 | it("should throw if \"window.localStorage.getItem\" is not available", (() => { 265 | spyGet.resetHistory(); 266 | window.localStorage.getItem = undefined; 267 | 268 | try { 269 | thisModule.get("foo"); 270 | } catch (err) { 271 | err.should.be.an("error"); 272 | } 273 | return true; 274 | })); 275 | 276 | it("should throw if we're missing the arguments", (() => { 277 | try { 278 | thisModule.get(); 279 | } catch (err) { 280 | err.should.be.an("error"); 281 | return true; 282 | } 283 | throw new Error("Failed"); 284 | })); 285 | 286 | it("should return the value from the \"lakka\" object stored at window.localStorage", (() => { 287 | window.localStorage.setItem("lakka", JSON.stringify({ 288 | "foo": { 289 | "bar": "baz" 290 | } 291 | })); 292 | let _result = thisModule.get("foo"); 293 | _result.should.deep.equal({ 294 | "bar": "baz" 295 | }); 296 | return true; 297 | })); 298 | 299 | it("should return the value from the \"lakka\" object stored at window.localStorage even if there are a multiple items", (() => { 300 | 301 | thisModule.set("foo", "foofoo"); 302 | thisModule.set("bar", { 303 | "foo": "bar" 304 | }); 305 | thisModule.set("baz", ["foo", "baz"]); 306 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 307 | "foo": "foofoo", 308 | "bar": { 309 | "foo": "bar" 310 | }, 311 | "baz": ["foo", "baz"] 312 | })); 313 | 314 | let _result = thisModule.get("bar"); 315 | _result.should.deep.equal({ 316 | "foo": "bar" 317 | }); 318 | return true; 319 | })); 320 | 321 | 322 | it("should return the value from window.localStorage - \"null\" if there's no value for the key", (() => { 323 | window.localStorage.setItem("lakka", JSON.stringify({ 324 | "foo": null 325 | })); 326 | let _result = thisModule.get("foo"); 327 | (_result === null).should.be.true; 328 | return true; 329 | })); 330 | 331 | it("should return the value from window.localStorage - \"null\" if there's no item for the key", (() => { 332 | window.localStorage.setItem("lakka", JSON.stringify({ 333 | "foo": null 334 | })); 335 | let _result = thisModule.get("bar"); 336 | (_result === null).should.be.true; 337 | return true; 338 | })); 339 | 340 | }); 341 | 342 | 343 | describe("should have an API \"del\". It:", () => { 344 | 345 | it("should return true if everything is fine", (() => { 346 | let _result; 347 | try { 348 | thisModule.set("foo", "foofoo"); 349 | spySet.resetHistory(); 350 | _result = thisModule.del("foo"); 351 | } catch (err) { 352 | console.error("error: ", err) 353 | } 354 | _result.should.be.ok; 355 | return true; 356 | })); 357 | 358 | it("should call \"window.localStorage.setItem\" with a \"lakka\" object cleaned of the item", (() => { 359 | try { 360 | thisModule.set("foo", "foofoo"); 361 | spySet.resetHistory(); 362 | thisModule.del("foo"); 363 | } catch (err) { 364 | console.error("error: ", err) 365 | } 366 | spySet.should.have.been.calledWith("lakka", JSON.stringify({})); 367 | return true; 368 | })); 369 | 370 | it("should call \"window.localStorage.setItem\" with a \"lakka\" object cleaned of the item and preserve existing", (() => { 371 | try { 372 | thisModule.set("foo", "foofoo"); 373 | thisModule.set("bar", { 374 | "foo": "bar" 375 | }); 376 | thisModule.set("baz", ["foo", "baz"]); 377 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 378 | "foo": "foofoo", 379 | "bar": { 380 | "foo": "bar" 381 | }, 382 | "baz": ["foo", "baz"] 383 | })); 384 | 385 | thisModule.del("foo"); 386 | } catch (err) { 387 | console.error("error: ", err) 388 | } 389 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 390 | "bar": { 391 | "foo": "bar" 392 | }, 393 | "baz": ["foo", "baz"] 394 | })); 395 | return true; 396 | })); 397 | 398 | 399 | it("should throw if we're missing the arguments", (() => { 400 | try { 401 | thisModule.del(); 402 | } catch (err) { 403 | err.should.be.an("error"); 404 | return true; 405 | } 406 | throw new Error("Failed"); 407 | })); 408 | 409 | }); 410 | 411 | describe("should have an API \"flush\". It:", () => { 412 | 413 | it("should return true if everything is fine", (() => { 414 | let _result; 415 | try { 416 | thisModule.set("foo", "foofoo"); 417 | spySet.resetHistory(); 418 | _result = thisModule.flush(); 419 | } catch (err) { 420 | console.error("error: ", err) 421 | } 422 | _result.should.be.ok; 423 | return true; 424 | })); 425 | 426 | it("should not call \"window.localStorage.clear\"", (() => { 427 | try { 428 | thisModule.flush(); 429 | } catch (err) { 430 | console.error("error: ", err) 431 | } 432 | spyFlush.should.not.have.been.called; 433 | return true; 434 | })); 435 | 436 | it("should call \"window.localStorage.setItem\" with an empty \"lakak\" object", (() => { 437 | try { 438 | thisModule.set("foo", "foofoo"); 439 | thisModule.set("bar", { 440 | "foo": "bar" 441 | }); 442 | thisModule.set("baz", ["foo", "baz"]); 443 | spySet.should.have.been.calledWith("lakka", JSON.stringify({ 444 | "foo": "foofoo", 445 | "bar": { 446 | "foo": "bar" 447 | }, 448 | "baz": ["foo", "baz"] 449 | })); 450 | spySet.resetHistory(); 451 | thisModule.flush(); 452 | } catch (err) { 453 | console.error("error: ", err) 454 | } 455 | spySet.should.have.been.calledWith("lakka", JSON.stringify({})); 456 | return true; 457 | })); 458 | }); 459 | 460 | 461 | }); 462 | -------------------------------------------------------------------------------- /test/api/before.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for the window.localstorage facade 3 | * This module provides a facade for accessing the internal private cache. 4 | * In this case: we're using the localStorage 5 | * 6 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 7 | * @license MIT license: https://opensource.org/licenses/MIT 8 | * 9 | * @author Martin Krause 10 | */ 11 | 12 | /* eslint-env mocha */ 13 | 14 | 15 | import thisModule from "./../../app/api/before"; 16 | import thisConfig from "./../../app/configuration/main"; 17 | import thisCreateKey from "./../../app/cache/create-key"; 18 | import thisCreateItem from "./../../app/cache/create-item"; 19 | 20 | const thisModulePath = "api/before"; 21 | // let thisModule; 22 | 23 | // // mock dependencies 24 | // const proxyquire = require("proxyquire").noCallThru(); 25 | // const stubAndReturn = ((value) => { 26 | // thisModule = proxyquire("./../../app/" + thisModulePath, 27 | // { 28 | // // "./../adapter/puppeteer": (item) => { return new Promise((resolve, reject) => {item.screenshot = value; resolve(item); }); } 29 | // } 30 | // ); 31 | // }); 32 | 33 | describe(`The module "${thisModulePath}"`, () => { 34 | 35 | before(() => { 36 | 37 | var _key1 = thisCreateKey("string"); 38 | var _key2 = thisCreateKey("/matchMe.html"); 39 | var _obj = {}; 40 | _obj[_key1] = thisCreateItem("string", "value-HTML"); 41 | _obj[_key2] = thisCreateItem("/matchMe.html", "matchMe-HTML"); 42 | global.window.localStorage.setItem("lakka", JSON.stringify(_obj)); 43 | // global.window.localStorage.setItem("/noMatch.html", "noMatch-HTML"); 44 | }); 45 | 46 | beforeEach(() => { 47 | // stubAndReturn(true); 48 | }) 49 | 50 | after(() => { 51 | global.window.localStorage.clear(); 52 | }) 53 | 54 | 55 | describe("should provide an unified API. It:", () => { 56 | 57 | it("should be a sync function", () => { 58 | (Object.getPrototypeOf(thisModule).constructor.name === "Function").should.be.ok; 59 | }); 60 | }); 61 | 62 | describe("should consume one argument. It:", () => { 63 | 64 | it("should take one argument, a \"String\"", (() => { 65 | try { 66 | thisModule("string"); 67 | } catch (err) { 68 | throw new Error("Failed"); 69 | } 70 | return true; 71 | })); 72 | 73 | it("should throw if the first argument is a boolean \"true\" and not a \"String\"", (() => { 74 | try { 75 | thisModule(true); 76 | } catch (err) { 77 | err.should.be.an("error"); 78 | return true; 79 | } 80 | throw new Error("Failed"); 81 | })); 82 | 83 | it("should throw if the first argument is a boolean \"false\" and not a \"String\"", (() => { 84 | try { 85 | thisModule(false); 86 | } catch (err) { 87 | err.should.be.an("error"); 88 | return true; 89 | } 90 | throw new Error("Failed"); 91 | })); 92 | 93 | it("should throw if the first argument is an \"Array\" and not a \"String\"", (() => { 94 | try { 95 | thisModule([]); 96 | } catch (err) { 97 | err.should.be.an("error"); 98 | return true; 99 | } 100 | throw new Error("Failed"); 101 | })); 102 | 103 | it("should throw if the first argument is an \"Object\" and not a \"String\"", (() => { 104 | try { 105 | thisModule({}); 106 | } catch (err) { 107 | err.should.be.an("error"); 108 | return true; 109 | } 110 | throw new Error("Failed"); 111 | })); 112 | 113 | it("should throw if the first argument is a \"Number\" and not a \"String\"", (() => { 114 | try { 115 | thisModule(1); 116 | } catch (err) { 117 | err.should.be.an("error"); 118 | return true; 119 | } 120 | throw new Error("Failed"); 121 | })); 122 | }); 123 | 124 | describe("should consume two arguments. It:", () => { 125 | 126 | it("should take two arguments, a \"String\" and an \"Object\"", (() => { 127 | try { 128 | thisModule("string", {}); 129 | } catch (err) { 130 | throw new Error("Failed"); 131 | } 132 | return true; 133 | })); 134 | 135 | it("should ignore the second if the second is \"null\"", (() => { 136 | try { 137 | thisModule("string", null); 138 | } catch (err) { 139 | throw new Error("Failed"); 140 | } 141 | return true; 142 | })); 143 | 144 | it("should ignore the second if the second is \"undefined\" ", (() => { 145 | try { 146 | thisModule("string", undefined); 147 | } catch (err) { 148 | throw new Error("Failed"); 149 | } 150 | return true; 151 | })); 152 | 153 | 154 | it("should throw if the second argument is a boolean \"true\" and not an \"Object\"", (() => { 155 | try { 156 | thisModule("string", true); 157 | } catch (err) { 158 | err.should.be.an("error"); 159 | return true; 160 | } 161 | throw new Error("Failed"); 162 | })); 163 | 164 | it("should throw if the second argument is a boolean \"false\" and not an \"Object\"", (() => { 165 | try { 166 | thisModule("string", false); 167 | } catch (err) { 168 | err.should.be.an("error"); 169 | return true; 170 | } 171 | throw new Error("Failed"); 172 | })); 173 | 174 | it("should throw if the second argument is an \"Array\" and not an \"Object\"", (() => { 175 | try { 176 | thisModule("string", []); 177 | } catch (err) { 178 | err.should.be.an("error"); 179 | return true; 180 | } 181 | throw new Error("Failed"); 182 | })); 183 | 184 | it("should throw if the second argument is a \"String\" and not an \"Object\"", (() => { 185 | try { 186 | thisModule("string", "string"); 187 | } catch (err) { 188 | err.should.be.an("error"); 189 | return true; 190 | } 191 | throw new Error("Failed"); 192 | })); 193 | 194 | it("should throw if the second argument is a \"Number\" and not an \"Object\"", (() => { 195 | try { 196 | thisModule("string", 1); 197 | } catch (err) { 198 | err.should.be.an("error"); 199 | return true; 200 | } 201 | throw new Error("Failed"); 202 | })); 203 | 204 | }); 205 | 206 | describe("should work with \"exclude\" & \"include\" patterns. It:", () => { 207 | 208 | beforeEach(() => { 209 | let _object = { 210 | "include": [], 211 | "exclude": [], 212 | "minutes": 60 213 | }; 214 | thisConfig.set(_object); 215 | }); 216 | 217 | after(() => { 218 | let _object = { 219 | "include": [], 220 | "exclude": [], 221 | "minutes": 60 222 | }; 223 | thisConfig.set(_object); 224 | }); 225 | 226 | it("should not throw if no pattern is set", (() => { 227 | try { 228 | thisModule("string"); 229 | } catch (err) { 230 | throw new Error("Failed"); 231 | } 232 | return true; 233 | })); 234 | 235 | it("should throw if the uri matches the \"exclude\" pattern", (() => { 236 | thisConfig.set("exclude", "matchMe"); 237 | try { 238 | thisModule("/matchMe.html"); 239 | } catch (err) { 240 | err.should.be.an("error"); 241 | return true; 242 | } 243 | throw new Error("Failed"); 244 | })); 245 | 246 | it("should throw if the uri matches the \"exclude\" pattern and not the \"include\" pattern", (() => { 247 | thisConfig.set("exclude", "matchMe"); 248 | thisConfig.set("include", "includeMe"); 249 | try { 250 | thisModule("/matchMe.html"); 251 | } catch (err) { 252 | err.should.be.an("error"); 253 | return true; 254 | } 255 | throw new Error("Failed"); 256 | })); 257 | 258 | it("should throw if the uri matches the \"exclude\" pattern and the \"include\" pattern (\"exclude\" takes precedence)", (() => { 259 | thisConfig.set("exclude", "matchMe"); 260 | thisConfig.set("include", "matchMe"); 261 | try { 262 | thisModule("/matchMe.html"); 263 | } catch (err) { 264 | err.should.be.an("error"); 265 | return true; 266 | } 267 | throw new Error("Failed"); 268 | })); 269 | 270 | 271 | it("should not throw if the uri matches the \"include\" pattern if one is set", (() => { 272 | thisConfig.set("include", "matchMe"); 273 | try { 274 | thisModule("/matchMe.html"); 275 | } catch (err) { 276 | console.log("err", err) 277 | throw new Error("Failed"); 278 | } 279 | return true; 280 | })); 281 | 282 | it("should throw if the uri does not matches the \"include\" pattern if one is set", (() => { 283 | thisConfig.set("include", "matchMe"); 284 | try { 285 | thisModule("/noMatch.html"); 286 | } catch (err) { 287 | err.should.be.an("error"); 288 | return true; 289 | } 290 | throw new Error("Failed"); 291 | })); 292 | 293 | }); 294 | 295 | describe("should consider the cache-control header. It:", () => { 296 | let _options; 297 | beforeEach(() => { 298 | _options = { 299 | "headers": { 300 | "Cache-Control": "" 301 | } 302 | }; 303 | }); 304 | 305 | after(() => { }); 306 | 307 | it("should not throw if there's no headers-property", (() => { 308 | try { 309 | delete _options.headers; 310 | thisModule("string", _options); 311 | } catch (err) { 312 | throw new Error("Failed"); 313 | } 314 | return true; 315 | })); 316 | 317 | it("should not throw if there's no Cache-Control-Header", (() => { 318 | try { 319 | delete _options.headers["Cache-Control"]; 320 | thisModule("string", _options); 321 | } catch (err) { 322 | throw new Error("Failed"); 323 | } 324 | return true; 325 | })); 326 | 327 | it("should not throw if the Cache-Control-Header is set to \"only-if-cached\"", (() => { 328 | try { 329 | _options.headers["Cache-Control"] = "only-if-cached"; 330 | thisModule("string", _options); 331 | } catch (err) { 332 | throw new Error("Failed"); 333 | } 334 | return true; 335 | 336 | })); 337 | 338 | it("should not throw if the Cache-Control-Header is set to \"immutable\"", (() => { 339 | try { 340 | _options.headers["Cache-Control"] = "immutable"; 341 | thisModule("string", _options); 342 | } catch (err) { 343 | throw new Error("Failed"); 344 | } 345 | return true; 346 | })); 347 | 348 | it("should not throw if the Cache-Control-Header is set to \"public\"", (() => { 349 | try { 350 | _options.headers["Cache-Control"] = "public"; 351 | thisModule("string", _options); 352 | } catch (err) { 353 | throw new Error("Failed"); 354 | } 355 | return true; 356 | })); 357 | 358 | it("should not throw if the Cache-Control-Header is set to \"private\"", (() => { 359 | try { 360 | _options.headers["Cache-Control"] = "private"; 361 | thisModule("string", _options); 362 | } catch (err) { 363 | throw new Error("Failed"); 364 | } 365 | return true; 366 | })); 367 | 368 | 369 | 370 | it("should throw if the Cache-Control-Header is set to \"must-revalidate\"", (() => { 371 | try { 372 | _options.headers["Cache-Control"] = "must-revalidate"; 373 | thisModule("string", _options); 374 | } catch (err) { 375 | err.should.be.an("error"); 376 | return true; 377 | } 378 | throw new Error("Failed"); 379 | })); 380 | 381 | it("should throw if the Cache-Control-Header is set to \"no-store\"", (() => { 382 | try { 383 | _options.headers["Cache-Control"] = "no-store"; 384 | thisModule("string", _options); 385 | } catch (err) { 386 | err.should.be.an("error"); 387 | return true; 388 | } 389 | throw new Error("Failed"); 390 | })); 391 | 392 | it("should throw if the Cache-Control-Header is set to \"no-cache\"", (() => { 393 | try { 394 | _options.headers["Cache-Control"] = "no-cache"; 395 | thisModule("string", _options); 396 | } catch (err) { 397 | err.should.be.an("error"); 398 | return true; 399 | } 400 | throw new Error("Failed"); 401 | })); 402 | 403 | it("should ignore upper/lowercase", (() => { 404 | try { 405 | delete _options.headers["Cache-Control"]; 406 | _options.headers["cache-control"] = "no-cache"; 407 | thisModule("string", _options); 408 | } catch (err) { 409 | err.should.be.an("error"); 410 | return true; 411 | } 412 | throw new Error("Failed"); 413 | })); 414 | 415 | }); 416 | 417 | 418 | // "application/json", "text/x-json", "text/plain", "text/html" 419 | describe("should consider the content-type header. It:", () => { 420 | let _options; 421 | beforeEach(() => { 422 | _options = { 423 | "headers": { 424 | "Content-Type": "" 425 | } 426 | }; 427 | }); 428 | 429 | after(() => { }); 430 | it("should not throw if the Content-Type-Header is set to \"application/json\"", (() => { 431 | try { 432 | _options.headers["Content-Type"] = "application/json"; 433 | thisModule("string", _options); 434 | } catch (err) { 435 | throw new Error("Failed"); 436 | } 437 | return true; 438 | })); 439 | 440 | it("should not throw if the Content-Type-Header is set to \"text/x-json\"", (() => { 441 | try { 442 | _options.headers["Content-Type"] = "text/x-json"; 443 | thisModule("string", _options); 444 | } catch (err) { 445 | throw new Error("Failed"); 446 | } 447 | return true; 448 | })); 449 | 450 | it("should not throw if the Content-Type-Header is set to \"text/plain\"", (() => { 451 | try { 452 | _options.headers["Content-Type"] = "text/plain"; 453 | thisModule("string", _options); 454 | } catch (err) { 455 | throw new Error("Failed"); 456 | } 457 | return true; 458 | })); 459 | 460 | it("should not throw if the Content-Type-Header is set to \"text/html\"", (() => { 461 | try { 462 | _options.headers["Content-Type"] = "text/html"; 463 | thisModule("string", _options); 464 | } catch (err) { 465 | throw new Error("Failed"); 466 | } 467 | return true; 468 | })); 469 | 470 | 471 | it("should throw if the Content-Type-Header is set to \"x-anything-else\"", (() => { 472 | try { 473 | _options.headers["Content-Type"] = "x-anything-else"; 474 | thisModule("string", _options); 475 | } catch (err) { 476 | err.should.be.an("error"); 477 | return true; 478 | } 479 | throw new Error("Failed"); 480 | })); 481 | 482 | 483 | it("should ignore upper/lowercase", (() => { 484 | try { 485 | delete _options.headers["Content-Type"]; 486 | _options.headers["content-type"] = "x-anything-else"; 487 | thisModule("string", _options); 488 | } catch (err) { 489 | err.should.be.an("error"); 490 | return true; 491 | } 492 | throw new Error("Failed"); 493 | })); 494 | }); 495 | 496 | 497 | // "application/json", "text/x-json", "text/plain", "text/html" 498 | describe("should consider the Accept header. It:", () => { 499 | let _options; 500 | beforeEach(() => { 501 | _options = { 502 | "headers": { 503 | "Accept": "" 504 | } 505 | }; 506 | }); 507 | 508 | after(() => { }); 509 | 510 | it("should not throw if the Accept-Header is set to \"application/json\"", (() => { 511 | try { 512 | _options.headers["Accept"] = "application/json"; 513 | thisModule("string", _options); 514 | } catch (err) { 515 | throw new Error("Failed"); 516 | } 517 | return true; 518 | })); 519 | 520 | it("should not throw if the Accept-Header is set to \"text/x-json\"", (() => { 521 | try { 522 | _options.headers["Accept"] = "text/x-json"; 523 | thisModule("string", _options); 524 | } catch (err) { 525 | throw new Error("Failed"); 526 | } 527 | return true; 528 | })); 529 | 530 | it("should not throw if the Accept-Header is set to \"text/plain\"", (() => { 531 | try { 532 | _options.headers["Accept"] = "text/plain"; 533 | thisModule("string", _options); 534 | } catch (err) { 535 | throw new Error("Failed"); 536 | } 537 | return true; 538 | })); 539 | 540 | it("should not throw if the Accept-Header is set to \"text/html\"", (() => { 541 | try { 542 | _options.headers["Accept"] = "text/html"; 543 | thisModule("string", _options); 544 | } catch (err) { 545 | throw new Error("Failed"); 546 | } 547 | return true; 548 | })); 549 | 550 | 551 | it("should throw if the Accept-Header is set to \"x-anything-else\"", (() => { 552 | try { 553 | _options.headers["Accept"] = "x-anything-else"; 554 | thisModule("string", _options); 555 | } catch (err) { 556 | err.should.be.an("error"); 557 | return true; 558 | } 559 | throw new Error("Failed"); 560 | })); 561 | 562 | it("should ignore upper/lowercase", (() => { 563 | try { 564 | delete _options.headers["Accept"]; 565 | _options.headers["accept"] = "x-anything-else"; 566 | thisModule("string", _options); 567 | } catch (err) { 568 | err.should.be.an("error"); 569 | return true; 570 | } 571 | throw new Error("Failed"); 572 | })); 573 | }); 574 | 575 | 576 | describe("should read and write from localStorage. It:", () => { 577 | 578 | 579 | beforeEach(() => { 580 | global.window.localStorage.clear(); 581 | }); 582 | 583 | after(() => { 584 | global.window.localStorage.clear(); 585 | }); 586 | 587 | it("should throw if there is no item for this uri", (() => { 588 | global.window.localStorage.setItem("lakka", JSON.stringify({ "string": "value" })); 589 | // global.window.localStorage._data = { string: "value" }; 590 | try { 591 | thisModule("string"); 592 | } catch (err) { 593 | err.should.be.an("error"); 594 | return true; 595 | } 596 | throw new Error("Failed"); 597 | })); 598 | 599 | it("should return the item for this uri if it's fresh", (() => { 600 | var _key1 = thisCreateKey("string"); 601 | var _obj = {}; 602 | _obj[_key1] = thisCreateItem("string", "value-HTML", { "Expires": (new Date(new Date(new Date().getTime() + (1000 * 60 * 60)).getTime())) }); 603 | global.window.localStorage.setItem("lakka", JSON.stringify(_obj)); 604 | thisModule("string").should.be.an("object"); 605 | })); 606 | 607 | it("should purge the cache from stale items", (() => { 608 | var _key1 = thisCreateKey("string"); 609 | var _key2 = thisCreateKey("string-2"); 610 | var _obj = {}; 611 | _obj[_key1] = thisCreateItem("string", "value-HTML", { "Expires": (new Date(new Date(new Date().getTime() - (1000 * 60 * 60)).getTime())) }); 612 | _obj[_key2] = thisCreateItem("string-2", "value-HTML", { "Expires": (new Date(new Date(new Date().getTime() + (1000 * 60 * 60)).getTime())) }); 613 | global.window.localStorage.setItem("lakka", JSON.stringify(_obj)); 614 | try { 615 | thisModule("string"); 616 | } catch (err) { 617 | _obj = JSON.parse(global.window.localStorage.getItem("lakka")); 618 | _obj.hasOwnProperty(_key2).should.be.true; 619 | _obj.hasOwnProperty(_key1).should.be.false; 620 | return true; 621 | } 622 | throw new Error("Failed"); 623 | })); 624 | 625 | it("should throw if the item for this uri is stale", (() => { 626 | var _key1 = thisCreateKey("string"); 627 | var _obj = {}; 628 | _obj[_key1] = thisCreateItem("string", "value-HTML", { "Expires": (new Date(new Date(new Date().getTime() - (1000 * 60 * 60)).getTime())) }); 629 | global.window.localStorage.setItem("lakka", JSON.stringify(_obj)); 630 | try { 631 | thisModule("string"); 632 | } catch (err) { 633 | err.should.be.an("error"); 634 | return true; 635 | } 636 | throw new Error("Failed"); 637 | })); 638 | 639 | }); 640 | 641 | }); 642 | -------------------------------------------------------------------------------- /test/api/after.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specs for the window.localstorage facade 3 | * This module provides a facade for accessing the internal private cache. 4 | * In this case: we're using the localStorage 5 | * 6 | * @copyright 2017 Martin Krause (http://martinkr.github.io) 7 | * @license MIT license: https://opensource.org/licenses/MIT 8 | * 9 | * @author Martin Krause 10 | */ 11 | 12 | /* eslint-env mocha */ 13 | 14 | import thisModule from "./../../app/api/after"; 15 | import thisConfig from "./../../app/configuration/main"; 16 | import thisCreateKey from "./../../app/cache/create-key"; 17 | import thisCreateItem from "./../../app/cache/create-item"; 18 | 19 | const thisModulePath = "api/after"; 20 | 21 | // let thisModule; 22 | 23 | 24 | // // mock dependencies 25 | // const proxyquire = require("proxyquire").noCallThru(); 26 | // const stubAndReturn = ((value) => { 27 | // thisModule = proxyquire("./../../app/" + thisModulePath, 28 | // { 29 | // // "./../adapter/puppeteer": (item) => { return new Promise((resolve, reject) => {item.screenshot = value; resolve(item); }); } 30 | // } 31 | // ); 32 | // }); 33 | 34 | describe(`The module "${thisModulePath}"`, () => { 35 | 36 | before(() => { 37 | global.window.localStorage.clear(); 38 | global.window.localStorage.setItem(thisCreateKey("string"), thisCreateItem("string", "value-HTML")); 39 | global.window.localStorage.setItem(thisCreateKey("/matchMe.html"), thisCreateItem("/matchMe.html", "matchMe-HTML")); 40 | // global.window.localStorage.setItem("/noMatch.html", "noMatch-HTML"); 41 | 42 | }); 43 | 44 | beforeEach(() => { 45 | // stubAndReturn(true); 46 | }) 47 | 48 | after(() => { }) 49 | 50 | 51 | describe("should provide an unified API. It:", () => { 52 | 53 | it("should be a sync function", () => { 54 | (Object.getPrototypeOf(thisModule).constructor.name === "Function").should.be.ok; 55 | }); 56 | }); 57 | 58 | describe("should consume multiple argument. It:", () => { 59 | 60 | it("should not throw if the first argument is a \"String\"", (() => { 61 | try { 62 | thisModule("string", "response", 200, {}); 63 | } catch (err) { 64 | throw new Error("Failed"); 65 | } 66 | return true; 67 | })); 68 | 69 | it("should throw if the first argument is a boolean \"true\" and not a \"String\"", (() => { 70 | try { 71 | thisModule(true, "response", 200, {}); 72 | } catch (err) { 73 | err.should.be.an("error"); 74 | return true; 75 | } 76 | throw new Error("Failed"); 77 | })); 78 | 79 | it("should throw if the first argument is a boolean \"false\" and not a \"String\"", (() => { 80 | try { 81 | thisModule(false, "response", 200, {}); 82 | } catch (err) { 83 | err.should.be.an("error"); 84 | return true; 85 | } 86 | throw new Error("Failed"); 87 | })); 88 | 89 | it("should throw if the first argument is an \"Array\" and not a \"String\"", (() => { 90 | try { 91 | thisModule([], "response", 200, {}); 92 | } catch (err) { 93 | err.should.be.an("error"); 94 | return true; 95 | } 96 | throw new Error("Failed"); 97 | })); 98 | 99 | it("should throw if the first argument is an \"Object\" and not a \"String\"", (() => { 100 | try { 101 | thisModule({}, "response", 200, {}); 102 | } catch (err) { 103 | err.should.be.an("error"); 104 | return true; 105 | } 106 | throw new Error("Failed"); 107 | })); 108 | 109 | it("should throw if the first argument is a \"Number\" and not a \"String\"", (() => { 110 | try { 111 | thisModule(1, "response", 200, {}); 112 | } catch (err) { 113 | err.should.be.an("error"); 114 | return true; 115 | } 116 | throw new Error("Failed"); 117 | })); 118 | 119 | 120 | it("should not throw if the second argument is a \"String\"", (() => { 121 | try { 122 | thisModule("string", "response", 200, {}); 123 | } catch (err) { 124 | throw new Error("Failed"); 125 | } 126 | return true; 127 | })); 128 | 129 | it("should throw if the second argument is a boolean \"true\" and not an \"string\"", (() => { 130 | try { 131 | thisModule("string", true, 200, {}); 132 | } catch (err) { 133 | err.should.be.an("error"); 134 | return true; 135 | } 136 | throw new Error("Failed"); 137 | })); 138 | 139 | it("should throw if the second argument is a boolean \"false\" and not an \"string\"", (() => { 140 | try { 141 | thisModule("string", false, 200, {}); 142 | } catch (err) { 143 | err.should.be.an("error"); 144 | return true; 145 | } 146 | throw new Error("Failed"); 147 | })); 148 | 149 | it("should throw if the second argument is an \"Array\" and not an \"string\"", (() => { 150 | try { 151 | thisModule("string", [], 200, {}); 152 | } catch (err) { 153 | err.should.be.an("error"); 154 | return true; 155 | } 156 | throw new Error("Failed"); 157 | })); 158 | 159 | it("should throw if the second argument is a \"Object\" and not an \"string\"", (() => { 160 | try { 161 | thisModule("string", {}, 200, {}); 162 | } catch (err) { 163 | err.should.be.an("error"); 164 | return true; 165 | } 166 | throw new Error("Failed"); 167 | })); 168 | 169 | it("should throw if the second argument is a \"Number\" and not an \"string\"", (() => { 170 | try { 171 | thisModule("string", 1, 200, {}); 172 | } catch (err) { 173 | err.should.be.an("error"); 174 | return true; 175 | } 176 | throw new Error("Failed"); 177 | })); 178 | 179 | 180 | 181 | it("should not throw if the third argument is a \"String\"", (() => { 182 | try { 183 | thisModule("string", "response", "200", {}); 184 | } catch (err) { 185 | throw new Error("Failed"); 186 | } 187 | return true; 188 | })); 189 | 190 | it("should not throw if the third argument is a \"Number\"", (() => { 191 | try { 192 | thisModule("string", "response", 200, {}); 193 | } catch (err) { 194 | throw new Error("Failed"); 195 | } 196 | return true; 197 | })); 198 | 199 | it("should throw if the third argument is a boolean \"true\" and not an \"string|number\"", (() => { 200 | try { 201 | thisModule("string", "response", true, {}); 202 | } catch (err) { 203 | err.should.be.an("error"); 204 | return true; 205 | } 206 | throw new Error("Failed"); 207 | })); 208 | 209 | it("should throw if the third argument is a boolean \"false\" and not an \"string|number\"", (() => { 210 | try { 211 | thisModule("string", "response", false, {}); 212 | } catch (err) { 213 | err.should.be.an("error"); 214 | return true; 215 | } 216 | throw new Error("Failed"); 217 | })); 218 | 219 | it("should throw if the third argument is an \"Array\" and not an \"string|number\"", (() => { 220 | try { 221 | thisModule("string", "response", [], {}); 222 | } catch (err) { 223 | err.should.be.an("error"); 224 | return true; 225 | } 226 | throw new Error("Failed"); 227 | })); 228 | 229 | it("should throw if the third argument is a \"Object\" and not an \"string|number\"", (() => { 230 | try { 231 | thisModule("string", "response", {}, {}); 232 | } catch (err) { 233 | err.should.be.an("error"); 234 | return true; 235 | } 236 | throw new Error("Failed"); 237 | })); 238 | 239 | 240 | it("should not throw if the fourth argument is an \"Object\"", (() => { 241 | try { 242 | thisModule("string", "response", 200, {}); 243 | } catch (err) { 244 | throw new Error("Failed"); 245 | } 246 | return true; 247 | })); 248 | 249 | it("should throw if the fourth argument is a boolean \"true\" and not an \"Object\"", (() => { 250 | try { 251 | thisModule("string", "response", 200, true); 252 | } catch (err) { 253 | err.should.be.an("error"); 254 | return true; 255 | } 256 | throw new Error("Failed"); 257 | })); 258 | 259 | it("should throw if the fourth argument is a boolean \"false\" and not an \"Object\"", (() => { 260 | try { 261 | thisModule("string", "response", 200, false); 262 | } catch (err) { 263 | err.should.be.an("error"); 264 | return true; 265 | } 266 | throw new Error("Failed"); 267 | })); 268 | 269 | it("should throw if the fourth argument is an \"Array\" and not an \"Object\"", (() => { 270 | try { 271 | thisModule("string", "response", 200, []); 272 | } catch (err) { 273 | err.should.be.an("error"); 274 | return true; 275 | } 276 | throw new Error("Failed"); 277 | })); 278 | 279 | it("should throw if the fourth argument is a \"String\" and not an \"Object\"", (() => { 280 | try { 281 | thisModule("string", "response", 200, "string"); 282 | } catch (err) { 283 | err.should.be.an("error"); 284 | return true; 285 | } 286 | throw new Error("Failed"); 287 | })); 288 | 289 | it("should throw if the fourth argument is a \"Number\" and not an \"Object\"", (() => { 290 | try { 291 | thisModule("string", "response", 200, 1); 292 | } catch (err) { 293 | err.should.be.an("error"); 294 | return true; 295 | } 296 | throw new Error("Failed"); 297 | })); 298 | 299 | 300 | }); 301 | 302 | 303 | 304 | describe("should work with \"exclude\" & \"include\" patterns. It:", () => { 305 | 306 | beforeEach(() => { 307 | let _object = { 308 | "include": [], 309 | "exclude": [], 310 | "minutes": 60 311 | }; 312 | thisConfig.set(_object); 313 | }); 314 | 315 | after(() => { 316 | let _object = { 317 | "include": [], 318 | "exclude": [], 319 | "minutes": 60 320 | }; 321 | thisConfig.set(_object); 322 | }); 323 | 324 | it("should not throw if no pattern is set", (() => { 325 | try { 326 | thisModule("string", "response", 200, {}); 327 | } catch (err) { 328 | throw new Error("Failed"); 329 | } 330 | return true; 331 | })); 332 | 333 | it("should throw if the uri matches the \"exclude\" pattern", (() => { 334 | thisConfig.set("exclude", "matchMe"); 335 | try { 336 | thisModule("/matchMe.html", "response", 200, {}); 337 | } catch (err) { 338 | err.should.be.an("error"); 339 | return true; 340 | } 341 | throw new Error("Failed"); 342 | })); 343 | 344 | it("should throw if the uri matches the \"exclude\" pattern and not the \"include\" pattern", (() => { 345 | thisConfig.set("exclude", "matchMe"); 346 | thisConfig.set("include", "includeMe"); 347 | try { 348 | thisModule("/matchMe.html", "response", 200, {}); 349 | } catch (err) { 350 | err.should.be.an("error"); 351 | return true; 352 | } 353 | throw new Error("Failed"); 354 | })); 355 | 356 | it("should throw if the uri matches the \"exclude\" pattern and the \"include\" pattern (\"exclude\" takes precedence)", (() => { 357 | thisConfig.set("exclude", "matchMe"); 358 | thisConfig.set("include", "matchMe"); 359 | try { 360 | thisModule("/matchMe.html", "response", 200, {}); 361 | } catch (err) { 362 | err.should.be.an("error"); 363 | return true; 364 | } 365 | throw new Error("Failed"); 366 | })); 367 | 368 | 369 | it("should not throw if the uri matches the \"include\" pattern if one is set", (() => { 370 | thisConfig.set("include", "matchMe"); 371 | try { 372 | thisModule("/matchMe.html", "response", 200, {}); 373 | } catch (err) { 374 | throw new Error("Failed"); 375 | } 376 | return true; 377 | })); 378 | 379 | it("should throw if the uri does not matches the \"include\" pattern if one is set", (() => { 380 | thisConfig.set("include", "matchMe"); 381 | try { 382 | thisModule("/noMatch.html", "response", 200, {}); 383 | } catch (err) { 384 | err.should.be.an("error"); 385 | return true; 386 | } 387 | throw new Error("Failed"); 388 | })); 389 | 390 | }); 391 | 392 | describe("should consider the status code. It:", () => { 393 | 394 | it("should not throw if the status code is \"200\"", (() => { 395 | try { 396 | thisModule("string", "response", 200, {}); 397 | } catch (err) { 398 | throw new Error("Failed"); 399 | } 400 | return true; 401 | })); 402 | 403 | it("should not throw if the status code is \"203\"", (() => { 404 | try { 405 | thisModule("string", "response", 203, {}); 406 | } catch (err) { 407 | throw new Error("Failed"); 408 | } 409 | return true; 410 | })); 411 | 412 | it("should not throw if the status code is \"226\"", (() => { 413 | try { 414 | thisModule("string", "response", 226, {}); 415 | } catch (err) { 416 | throw new Error("Failed"); 417 | } 418 | return true; 419 | })); 420 | 421 | it("should not throw if the status code is not valid", (() => { 422 | try { 423 | thisModule("string", "response", 404, {}); 424 | } catch (err) { 425 | err.should.be.an("error"); 426 | return true; 427 | } 428 | throw new Error("Failed"); 429 | })); 430 | 431 | }); 432 | 433 | 434 | describe("should consider the cache-control header. It:", () => { 435 | let _options; 436 | beforeEach(() => { 437 | _options = { 438 | "headers": { 439 | "Cache-Control": "" 440 | } 441 | }; 442 | }); 443 | 444 | after(() => { }); 445 | 446 | it("should not throw if there's no headers-property", (() => { 447 | try { 448 | delete _options.headers; 449 | thisModule("string", "response", 200, _options); 450 | } catch (err) { 451 | throw new Error("Failed"); 452 | } 453 | return true; 454 | })); 455 | 456 | it("should not throw if there's no Cache-Control-Header", (() => { 457 | try { 458 | delete _options.headers["Cache-Control"]; 459 | thisModule("string", "response", 200, _options); 460 | } catch (err) { 461 | throw new Error("Failed"); 462 | } 463 | return true; 464 | })); 465 | 466 | it("should not throw if the Cache-Control-Header is set to \"only-if-cached\"", (() => { 467 | try { 468 | _options.headers["Cache-Control"] = "only-if-cached"; 469 | thisModule("string", "response", 200, _options); 470 | } catch (err) { 471 | throw new Error("Failed"); 472 | } 473 | return true; 474 | 475 | })); 476 | 477 | it("should not throw if the Cache-Control-Header is set to \"immutable\"", (() => { 478 | try { 479 | _options.headers["Cache-Control"] = "immutable"; 480 | thisModule("string", "response", 200, _options); 481 | } catch (err) { 482 | throw new Error("Failed"); 483 | } 484 | return true; 485 | })); 486 | 487 | it("should not throw if the Cache-Control-Header is set to \"public\"", (() => { 488 | try { 489 | _options.headers["Cache-Control"] = "public"; 490 | thisModule("string", "response", 200, _options); 491 | } catch (err) { 492 | throw new Error("Failed"); 493 | } 494 | return true; 495 | })); 496 | 497 | it("should not throw if the Cache-Control-Header is set to \"private\"", (() => { 498 | try { 499 | _options.headers["Cache-Control"] = "private"; 500 | thisModule("string", "response", 200, _options); 501 | } catch (err) { 502 | throw new Error("Failed"); 503 | } 504 | return true; 505 | })); 506 | 507 | 508 | 509 | it("should throw if the Cache-Control-Header is set to \"must-revalidate\"", (() => { 510 | try { 511 | _options.headers["Cache-Control"] = "must-revalidate"; 512 | thisModule("string", "response", 200, _options); 513 | } catch (err) { 514 | err.should.be.an("error"); 515 | return true; 516 | } 517 | throw new Error("Failed"); 518 | })); 519 | 520 | it("should throw if the Cache-Control-Header is set to \"no-store\"", (() => { 521 | try { 522 | _options.headers["Cache-Control"] = "no-store"; 523 | thisModule("string", "response", 200, _options); 524 | } catch (err) { 525 | err.should.be.an("error"); 526 | return true; 527 | } 528 | throw new Error("Failed"); 529 | })); 530 | 531 | it("should throw if the Cache-Control-Header is set to \"no-cache\"", (() => { 532 | try { 533 | _options.headers["Cache-Control"] = "no-cache"; 534 | thisModule("string", "response", 200, _options); 535 | } catch (err) { 536 | err.should.be.an("error"); 537 | return true; 538 | } 539 | throw new Error("Failed"); 540 | })); 541 | 542 | it("should ignore upper/lowercase", (() => { 543 | try { 544 | delete _options.headers["Cache-Control"]; 545 | _options.headers["cache-control"] = "no-cache"; 546 | thisModule("string", "response", 200, _options); 547 | } catch (err) { 548 | err.should.be.an("error"); 549 | return true; 550 | } 551 | throw new Error("Failed"); 552 | })); 553 | 554 | }); 555 | 556 | 557 | // "application/json", "text/x-json", "text/plain", "text/html" 558 | describe("should consider the content-type header. It:", () => { 559 | let _options; 560 | beforeEach(() => { 561 | _options = { 562 | "headers": { 563 | "Content-Type": "" 564 | } 565 | }; 566 | }); 567 | 568 | after(() => { }); 569 | it("should not throw if the Content-Type-Header is set to \"application/json\"", (() => { 570 | try { 571 | _options.headers["Content-Type"] = "application/json"; 572 | thisModule("string", "response", 200, _options) 573 | } catch (err) { 574 | throw new Error("Failed"); 575 | } 576 | return true; 577 | })); 578 | 579 | it("should not throw if the Content-Type-Header is set to \"text/x-json\"", (() => { 580 | try { 581 | _options.headers["Content-Type"] = "text/x-json"; 582 | thisModule("string", "response", 200, _options) 583 | } catch (err) { 584 | throw new Error("Failed"); 585 | } 586 | return true; 587 | })); 588 | 589 | it("should not throw if the Content-Type-Header is set to \"text/plain\"", (() => { 590 | try { 591 | _options.headers["Content-Type"] = "text/plain"; 592 | thisModule("string", "response", 200, _options) 593 | } catch (err) { 594 | throw new Error("Failed"); 595 | } 596 | return true; 597 | })); 598 | 599 | it("should not throw if the Content-Type-Header is set to \"text/html\"", (() => { 600 | try { 601 | _options.headers["Content-Type"] = "text/html"; 602 | thisModule("string", "response", 200, _options) 603 | } catch (err) { 604 | throw new Error("Failed"); 605 | } 606 | return true; 607 | })); 608 | 609 | 610 | it("should throw if the Content-Type-Header is set to \"x-anything-else\"", (() => { 611 | try { 612 | _options.headers["Content-Type"] = "x-anything-else"; 613 | thisModule("string", "response", 200, _options) 614 | } catch (err) { 615 | err.should.be.an("error"); 616 | return true; 617 | } 618 | throw new Error("Failed"); 619 | })); 620 | 621 | 622 | it("should ignore upper/lowercase", (() => { 623 | try { 624 | delete _options.headers["Content-Type"]; 625 | _options.headers["content-type"] = "x-anything-else"; 626 | thisModule("string", "response", 200, _options) 627 | } catch (err) { 628 | err.should.be.an("error"); 629 | return true; 630 | } 631 | throw new Error("Failed"); 632 | })); 633 | }); 634 | 635 | 636 | // "application/json", "text/x-json", "text/plain", "text/html" 637 | describe("should consider the Cache-Control and the Expires header to see if the response is already stale. It:", () => { 638 | let _options; 639 | beforeEach(() => { 640 | _options = { 641 | "headers": { 642 | "Cache-Control": "" 643 | } 644 | }; 645 | }); 646 | 647 | after(() => { }); 648 | 649 | it("should not throw if the Expires-Header has a future date", (() => { 650 | try { 651 | delete _options.headers["Cache-Control"]; 652 | delete _options.headers["Expires"]; 653 | _options.headers["Expires"] = "" + new Date(new Date(new Date().getTime() + (1000 * 60 * 60)).getTime()); 654 | thisModule("string", "response", 200, _options); 655 | } catch (err) { 656 | throw new Error("Failed"); 657 | } 658 | return true; 659 | 660 | })); 661 | 662 | it("should not throw if theres no Expires-Header", (() => { 663 | try { 664 | delete _options.headers["Cache-Control"]; 665 | delete _options.headers["Expires"]; 666 | thisModule("string", "response", 200, _options); 667 | } catch (err) { 668 | throw new Error("Failed"); 669 | } 670 | return true; 671 | 672 | })); 673 | 674 | it("should throw if the Expires-Header has a stale date", (() => { 675 | try { 676 | delete _options.headers["Cache-Control"]; 677 | _options.headers["Expires"] = "" + new Date(new Date(new Date().getTime() - (1000 * 60 * 60)).getTime()); 678 | thisModule("string", "response", 200, _options); 679 | } catch (err) { 680 | err.should.be.an("error"); 681 | return true; 682 | } 683 | throw new Error("Failed"); 684 | 685 | })); 686 | }); 687 | 688 | 689 | 690 | describe("should work as expected. It:", () => { 691 | 692 | let _options = {}; 693 | 694 | beforeEach(() => { 695 | _options = {}; 696 | global.window.localStorage.clear(); 697 | }); 698 | 699 | after(() => { 700 | global.window.localStorage.clear(); 701 | }); 702 | 703 | it("should write to localStorage and be ok", (() => { 704 | global.window.localStorage.clear(); 705 | thisModule("string", "response", 200, _options); 706 | JSON.parse(global.window.localStorage.getItem("lakka"))["string"].should.be.ok; 707 | })); 708 | 709 | it("should return the cacheItem and be ok", (() => { 710 | global.window.localStorage.clear(); 711 | thisModule("string", "response", 200, _options).should.be.ok; 712 | })); 713 | 714 | 715 | }); 716 | 717 | }); 718 | --------------------------------------------------------------------------------