├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── node-aught.yml │ ├── node-pretest.yml │ ├── node-tens.yml │ ├── rebase.yml │ └── require-allow-edits.yml ├── .gitignore ├── .npmrc ├── .nycrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "extends": "@ljharb", 5 | 6 | "rules": { 7 | "func-name-matching": 1, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ljharb] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: npm/global-cache 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/node-aught.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js < 10' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | uses: ljharb/actions/.github/workflows/node.yml@main 8 | with: 9 | range: '< 10' 10 | type: minors 11 | command: npm run tests-only 12 | 13 | node: 14 | name: 'node < 10' 15 | needs: [tests] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: 'echo tests completed' 19 | -------------------------------------------------------------------------------- /.github/workflows/node-pretest.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: pretest/posttest' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | uses: ljharb/actions/.github/workflows/pretest.yml@main 8 | -------------------------------------------------------------------------------- /.github/workflows/node-tens.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js >= 10' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | uses: ljharb/actions/.github/workflows/node.yml@main 8 | with: 9 | range: '>= 10' 10 | type: minors 11 | command: npm run tests-only 12 | 13 | node: 14 | name: 'node >= 10' 15 | needs: [tests] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: 'echo tests completed' 19 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Rebase 2 | 3 | on: [pull_request_target] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | _: 10 | permissions: 11 | contents: write # for ljharb/rebase to push code to rebase 12 | pull-requests: read # for ljharb/rebase to get info about PR 13 | 14 | name: "Automatic Rebase" 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: ljharb/rebase@master 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/require-allow-edits.yml: -------------------------------------------------------------------------------- 1 | name: Require “Allow Edits” 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | _: 7 | name: "Require “Allow Edits”" 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: ljharb/require-allow-edits@main 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | .nyc_output/ 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | coverage/ 31 | 32 | # Only apps should have lockfiles 33 | yarn.lock 34 | package-lock.json 35 | npm-shrinkwrap.json 36 | 37 | .npmignore 38 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": false, 4 | "reporter": ["text-summary", "text", "html", "json"], 5 | "exclude": [ 6 | "coverage", 7 | "test" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.2.1 / 2017-07-20 2 | ================= 3 | * [Fix] `set`: actually overwrite values 4 | * [Tests] up to `node` `v8.1`, `v7.10`, `v6.11`; npm < 5 breaks on node < 4; improve matrix 5 | * [Dev Deps] update `tape`, `jscs`, `nsp`, `eslint`, `@ljharb/eslint-config`, `semver`, `rimraf` 6 | * [Dev Deps] remove unused dep `parallelshell` 7 | 8 | 1.2.0 / 2016-04-06 9 | ================= 10 | * [New] add `setIfMissingThenGet` 11 | * [Tests] Add istanbul for code coverage and run it as part of tests. 12 | * [Tests] add tests for exceptions 13 | * [Tests] use pretest/posttest for linting/security; add `--silent` 14 | * [Tests] up to `node` `v5.10`, `v4.4` 15 | * [Dev Deps] update `jscs`, `eslint`, `@ljharb/eslint-config`, `nsp` 16 | 17 | 1.1.0 / 2016-01-29 18 | ================= 19 | * [New] add `#clear()` to clear the cache 20 | * [Dev Deps] update `tape`, `jscs`, `nsp`, `eslint`, `@ljharb/eslint-config`, `semver` 21 | * [Tests] up to `node` `v5.5`, don’t allow `0.8` to fail 22 | * [Tests] fix npm upgrades on older nodes 23 | 24 | 1.0.3 / 2015-10-19 25 | ================= 26 | * [Robustness] Ensure that when `Symbol` and `Symbol.for` are available, they're real Symbols and not gross fake ones 27 | * package.json: use object form of "authors", add "contributors" 28 | * [Dev Deps] update `jscs`, `eslint`, `@ljharb/eslint-config` 29 | 30 | 1.0.2 / 2015-10-14 31 | ================= 32 | * [Deps] update `define-properties` 33 | * [Dev Deps] update `tape`, `jscs`, `eslint`, `@ljharb/eslint-config`, `nsp`, `semver` 34 | * [Tests] up to `io.js` `v3.3`, `node` `v4.2` 35 | * [Docs] Switch from vb.teelaun.ch to versionbadg.es for the npm version badge SVG 36 | 37 | 1.0.1 / 2015-08-12 38 | ================= 39 | * [Fix] Use `Symbol.for` to ensure that multiple instances of `global-cache` on the page share the same cache. 40 | * [Tests] Test up to `io.js` `v3.0` 41 | * [Dev Deps] Update `jscs`, `tape`, `eslint`, `semver`, `define-properties`; use my personal shared `eslint` config 42 | 43 | 1.0.0 / 2015-07-01 44 | ================= 45 | * Initial release. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jordan Harband 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # global-cache [![Version Badge][npm-version-svg]][package-url] 2 | 3 | [![github actions][actions-image]][actions-url] 4 | [![coverage][codecov-image]][codecov-url] 5 | [![dependency status][deps-svg]][deps-url] 6 | [![dev dependency status][dev-deps-svg]][dev-deps-url] 7 | [![License][license-image]][license-url] 8 | [![Downloads][downloads-image]][downloads-url] 9 | 10 | [![npm badge][npm-badge-png]][package-url] 11 | 12 | Sometimes you have to do horrible things, like use the global object to share a singleton. Abstract that away, with this! 13 | 14 | This attaches a cache to the global object. It attempts to make it as undiscoverable as possible: 15 | - uses Symbols if available 16 | - if not, uses a string key that is not a valid identifier, so as not to show up in dot-notation browser autocomplete 17 | - makes it non-enumerable if property descriptors are supported 18 | 19 | Keys are required to be strings or symbols. 20 | 21 | ## Example 22 | 23 | ```js 24 | var cache = require('global-cache'); 25 | var assert = require('assert'); 26 | 27 | var value = {}; 28 | assert(cache.get(key) === undefined); 29 | assert(cache.has(key) === false); 30 | 31 | cache.set(key, value); 32 | assert(cache.get(key) === value); 33 | assert(cache.has(key) === true); 34 | 35 | cache.delete(key); 36 | assert(cache.get(key) === undefined); 37 | assert(cache.has(key) === false); 38 | ``` 39 | 40 | ## Tests 41 | Simply clone the repo, `npm install`, and run `npm test` 42 | 43 | [package-url]: https://npmjs.org/package/global-cache 44 | [npm-version-svg]: https://versionbadg.es/ljharb/global-cache.svg 45 | [deps-svg]: https://david-dm.org/ljharb/global-cache.svg 46 | [deps-url]: https://david-dm.org/ljharb/global-cache 47 | [dev-deps-svg]: https://david-dm.org/ljharb/global-cache/dev-status.svg 48 | [dev-deps-url]: https://david-dm.org/ljharb/global-cache#info=devDependencies 49 | [npm-badge-png]: https://nodei.co/npm/global-cache.png?downloads=true&stars=true 50 | [license-image]: https://img.shields.io/npm/l/global-cache.svg 51 | [license-url]: LICENSE 52 | [downloads-image]: https://img.shields.io/npm/dm/global-cache.svg 53 | [downloads-url]: https://npm-stat.com/charts.html?package=global-cache 54 | [codecov-image]: https://codecov.io/gh/ljharb/global-cache/branch/main/graphs/badge.svg 55 | [codecov-url]: https://app.codecov.io/gh/ljharb/global-cache/ 56 | [actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/global-cache 57 | [actions-url]: https://github.com/ljharb/global-cache/actions 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var define = require('define-properties'); 4 | var isSymbol = require('is-symbol'); 5 | 6 | var globalKey = '__ global cache key __'; 7 | /* istanbul ignore else */ 8 | // eslint-disable-next-line no-restricted-properties 9 | if (typeof Symbol === 'function' && isSymbol(Symbol('foo')) && typeof Symbol['for'] === 'function') { 10 | // eslint-disable-next-line no-restricted-properties 11 | globalKey = Symbol['for'](globalKey); 12 | } 13 | 14 | var trueThunk = function () { 15 | return true; 16 | }; 17 | 18 | var ensureCache = function ensureCache() { 19 | if (!global[globalKey]) { 20 | var properties = {}; 21 | properties[globalKey] = {}; 22 | var predicates = {}; 23 | predicates[globalKey] = trueThunk; 24 | define(global, properties, predicates); 25 | } 26 | return global[globalKey]; 27 | }; 28 | 29 | var cache = ensureCache(); 30 | 31 | var isPrimitive = function isPrimitive(val) { 32 | return val === null || (typeof val !== 'object' && typeof val !== 'function'); 33 | }; 34 | 35 | var getPrimitiveKey = function getPrimitiveKey(val) { 36 | if (isSymbol(val)) { 37 | return Symbol.prototype.valueOf.call(val); 38 | } 39 | return typeof val + ' | ' + String(val); 40 | }; 41 | 42 | var requirePrimitiveKey = function requirePrimitiveKey(val) { 43 | if (!isPrimitive(val)) { 44 | throw new TypeError('key must not be an object'); 45 | } 46 | }; 47 | 48 | var globalCache = { 49 | clear: function clear() { 50 | delete global[globalKey]; 51 | cache = ensureCache(); 52 | }, 53 | 54 | 'delete': function deleteKey(key) { 55 | requirePrimitiveKey(key); 56 | delete cache[getPrimitiveKey(key)]; 57 | return !globalCache.has(key); 58 | }, 59 | 60 | get: function get(key) { 61 | requirePrimitiveKey(key); 62 | return cache[getPrimitiveKey(key)]; 63 | }, 64 | 65 | has: function has(key) { 66 | requirePrimitiveKey(key); 67 | return getPrimitiveKey(key) in cache; 68 | }, 69 | 70 | set: function set(key, value) { 71 | requirePrimitiveKey(key); 72 | var primitiveKey = getPrimitiveKey(key); 73 | var props = {}; 74 | props[primitiveKey] = value; 75 | var predicates = {}; 76 | predicates[primitiveKey] = trueThunk; 77 | define(cache, props, predicates); 78 | return globalCache.has(key); 79 | }, 80 | 81 | setIfMissingThenGet: function setIfMissingThenGet(key, valueThunk) { 82 | if (globalCache.has(key)) { 83 | return globalCache.get(key); 84 | } 85 | var item = valueThunk(); 86 | globalCache.set(key, item); 87 | return item; 88 | } 89 | }; 90 | 91 | module.exports = globalCache; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "global-cache", 3 | "version": "1.2.1", 4 | "author": { 5 | "name": "Jordan Harband", 6 | "email": "ljharb@gmail.com", 7 | "url": "http://ljharb.codes" 8 | }, 9 | "funding": { 10 | "url": "https://github.com/sponsors/ljharb" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Jordan Harband", 15 | "email": "ljharb@gmail.com", 16 | "url": "http://ljharb.codes" 17 | } 18 | ], 19 | "description": "Sometimes you have to do horrible things, like use the global object to share a singleton. Abstract that away, with this!", 20 | "license": "MIT", 21 | "main": "index.js", 22 | "scripts": { 23 | "prepack": "npmignore --auto --commentLines=autogenerated", 24 | "prepublishOnly": "safe-publish-latest", 25 | "prepublish": "not-in-publish || npm run prepublishOnly", 26 | "pretest": "npm run lint", 27 | "test": "npm run tests-only", 28 | "posttest": "aud --production", 29 | "tests-only": "nyc tape 'test/**/*.js'", 30 | "lint": "eslint --ext=js,mjs ." 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/ljharb/global-cache.git" 35 | }, 36 | "keywords": [ 37 | "global", 38 | "window", 39 | "self", 40 | "cache", 41 | "global object" 42 | ], 43 | "dependencies": { 44 | "define-properties": "^1.1.4", 45 | "is-symbol": "^1.0.4" 46 | }, 47 | "devDependencies": { 48 | "@ljharb/eslint-config": "^21.0.0", 49 | "aud": "^2.0.1", 50 | "eslint": "=8.8.0", 51 | "npmignore": "^0.3.0", 52 | "nyc": "^10.3.2", 53 | "rimraf": "^2.7.1", 54 | "safe-publish-latest": "^2.0.0", 55 | "tape": "^5.6.1" 56 | }, 57 | "testling": { 58 | "files": "test/index.js", 59 | "browsers": [ 60 | "iexplore/6.0..latest", 61 | "firefox/3.0..6.0", 62 | "firefox/15.0..latest", 63 | "firefox/nightly", 64 | "chrome/4.0..10.0", 65 | "chrome/20.0..latest", 66 | "chrome/canary", 67 | "opera/10.0..latest", 68 | "opera/next", 69 | "safari/4.0..latest", 70 | "ipad/6.0..latest", 71 | "iphone/6.0..latest", 72 | "android-browser/4.2" 73 | ] 74 | }, 75 | "engines": { 76 | "node": ">= 0.4" 77 | }, 78 | "publishConfig": { 79 | "ignore": [ 80 | ".github/workflows" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var globalCache = require('..'); 5 | var hasSymbols = typeof Symbol === 'function' && typeof Symbol('foo') === 'symbol'; 6 | 7 | test('exceptions', function (t) { 8 | t['throws'](function () { globalCache.get({}); }, '`get` throws on non-primitive key'); 9 | t['throws'](function () { globalCache.set({}); }, '`set` throws on non-primitive key'); 10 | t['throws'](function () { globalCache.has({}); }, '`has` throws on non-primitive key'); 11 | t['throws'](function () { globalCache['delete']({}); }, '`delete` throws on non-primitive key'); 12 | t['throws'](function () { globalCache.setIfMissingAndGet({}); }, '`setIfMissingAndGet` throws on non-primitive key'); 13 | 14 | t.end(); 15 | }); 16 | 17 | test('basic usage', function (t) { 18 | var key = 'foo'; 19 | var bar = { baz: 'quux' }; 20 | var baz = { quux: 'xyzzy' }; 21 | 22 | t.notOk(globalCache.has(key), 'global cache starts out without key'); 23 | 24 | t.ok(globalCache.set(key, bar), 'global cache is able to set key'); 25 | t.ok(globalCache.has(key), 'global cache has key'); 26 | t.equal(globalCache.get(key), bar, 'global cache returns value for key'); 27 | 28 | t.ok(globalCache.set(key, baz), 'global cache is able to re-set key'); 29 | t.ok(globalCache.has(key), 'global cache still has key'); 30 | t.equal(globalCache.get(key), baz, 'global cache returns new value for key'); 31 | 32 | t.notOk(key in global, 'key is not in global object'); 33 | 34 | t.ok(globalCache['delete'](key), 'global cache can delete key'); 35 | t.notOk(globalCache.has(key), 'global cache does not have key'); 36 | t.equal(globalCache.get(key), undefined, 'global cache returns undefined for key'); 37 | 38 | globalCache.set(key, bar); 39 | t.ok(globalCache.has(key), 'global cache has key before clear'); 40 | globalCache.clear(); 41 | t.notOk(globalCache.has(key), 'global cache does not have key after clear'); 42 | 43 | t.end(); 44 | }); 45 | 46 | test('.setIfMissingThenGet()', function (t) { 47 | var key = 'missing'; 48 | var a = {}; 49 | var b = {}; 50 | var values = [a, b]; 51 | var thunk = function () { return values.pop(); }; 52 | 53 | t.notOk(globalCache.has(key), 'global cache starts out without key'); 54 | t.equal(globalCache.setIfMissingThenGet(key, thunk), b, 'global cache sets result of thunk'); 55 | t.deepEqual(values, [a], 'values array has last item popped off'); 56 | 57 | t.equal(globalCache.setIfMissingThenGet(key, thunk), b, 'global cache skips thunk when has key'); 58 | t.deepEqual(values, [a], 'values array is unchanged'); 59 | 60 | t.ok(globalCache['delete'](key), 'global cache can delete key'); 61 | t.equal(globalCache.setIfMissingThenGet(key, thunk), a, 'global cache sets result of thunk'); 62 | t.deepEqual(values, [], 'values array has last item popped off'); 63 | 64 | t.end(); 65 | }); 66 | 67 | test('symbols', { skip: !hasSymbols }, function (t) { 68 | var sym = Symbol('foo'); 69 | var bar = { baz: 'quux' }; 70 | 71 | t.notOk(globalCache.has(sym), 'global cache starts out without symbol key'); 72 | 73 | t.ok(globalCache.set(sym, bar), 'global cache is able to set symbol key'); 74 | t.ok(globalCache.has(sym), 'global cache has symbol key'); 75 | t.equal(globalCache.get(sym), bar, 'global cache returns value for symbol key'); 76 | 77 | t.notOk(sym in global, 'symbol key is not in global object'); 78 | 79 | t.ok(globalCache['delete'](sym), 'global cache can delete symbol key'); 80 | t.notOk(globalCache.has(sym), 'global cache does not have symbol key'); 81 | t.equal(globalCache.get(sym), undefined, 'global cache returns undefined for symbol key'); 82 | 83 | t.test('when the module is included twice', { skip: !require.cache }, function (st) { 84 | Object.keys(require.cache).some(function (id) { 85 | if (require.cache[id].exports === globalCache) { 86 | delete require.cache[id]; 87 | return true; 88 | } 89 | return false; 90 | }); 91 | // eslint-disable-next-line global-require 92 | var globalCache2 = require('..'); 93 | st.notEqual(globalCache, globalCache2, 'both cache objects are different'); 94 | 95 | globalCache.set('foo', bar); 96 | var bar2 = globalCache2.get('foo'); 97 | st.equal(bar2, bar, 'global cache 2 can retrieve things global cache 1 sets'); 98 | st.end(); 99 | }); 100 | 101 | t.end(); 102 | }); 103 | --------------------------------------------------------------------------------