diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..efce947
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+/src/app/system/console/colors.package.js~
+/package.json~
+/docs/RULES.md~
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
+node_modules
+
+# Compiled api docs
+api_docs
+/log.txt
+/bin
+/stacks.out
+/out
+yarn-error.log
+NTH
+
+docs
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..f284bb9
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,3 @@
+tests
+README.md
+.travis.yml
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..501e1d0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+language: node_js
+node_js:
+ - "10"
+ - "8"
+ - "6"
+install:
+ - npm ci
+script:
+ - npm test
+ - npm run bench
+sudo: false
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0849b8e
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,73 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at fredericcharette@gmail.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..4fd506e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,8 @@
+# Contribute
+
+Please contribute! Here are some things that would be great:
+- Open an issue!
+- Open a pull request!
+- Say hi! :wave:
+
+Please abide by the [code of conduct](CODE_OF_CONDUCT.md).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1b72f84
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2018 Frederic Charette
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/README.md b/README.md
index a56d87b..d01d075 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,36 @@
-# REST-store
+
+
+
+
+
+ REST-store
+
+
+ The resource Manager
+
+
+
+
+[](https://www.npmjs.com/package/rest-store)
+[](https://nodejs.org)
+[](https://travis-ci.org/fed135/rest-store.js)
+[](https://david-dm.org/fed135/rest-store.js)
+
+---
+
+## How it works
Want to make your app faster and don't want to spend on extra infrastructure ?
**REST-store** is:
-An in-memory, self-adjustting cache:
+An in-memory, self-adjustting microcache:
- Helps reduce the number of requests for 'hot' information
- No noticeable footprint
- No need for extra caching architecture (redis/memcache)
-With request dedupping, batching, retrying and circuit-breaking:
+Adds request dedupping, batching, retrying and circuit-breaking:
- Process-wide request profiling and mapping
- Greatly reduces the number of requests
@@ -30,15 +50,86 @@ With request dedupping, batching, retrying and circuit-breaking:
## Usage
+**Data-Access layer**
+
```node
+function getItems(ids, params) {
+ // Some compute-heavy async function or external request to a DB / service
+}
+```
+**Store**
+```node
+const store = require('rest-store');
+const itemStore = store({ getter: { method: getItems }});
```
+**Model**
+```node
+function getItemById(id, params) {
+ itemStore.one(id, params)
+ .then(item => /* The item you requested */);
+}
+```
+
+## Options
+
+Name | Required | Default | Description
+--- | --- | --- |
+getter | true | - | The method to wrap, and how to interpret the returned data. Uses the format `{ method: , responseParser: `
+uniqueOptions | false | `[]` | The list of parameters that, when passed, alter the results of the items requested. Ex: 'language', 'view', 'fields', 'country'. These will generate different combinaisons of cache keys.
+cache | false | ```{
+ enabled: true,
+ step: 1000,
+ ttl: 10000,
+}``` | Caching options for the data
+batch | false | ```{
+ enabled: false,
+ tick: 40,
+ limit: 100,
+}``` | Batching options for the requests
+retry | false | ```{
+ enabled: true,
+ max: 3,
+ scale: {
+ mult: 2.5,
+ base: 5,
+ }
+}``` | Retry options for the requests
+
+
+## Monitoring and events
+
+REST-store emits events to track cache hits, miss and outbound requests.
+
+Event | Description
+--- | ---
+cacheHit | When the requested item is present in the microcache, or is already being fetched. Prevents another request from being created.
+cacheMiss | When the requested item is not present in the microcache and is not currently being fetched. A new request will be made.
+batch | When a batch of requests is about to be sent.
+batchFailed | Indicates that the batch has failed. Retry policy will dictate if it should be re-attempted.
+batchCancelled | Indicates that the batch has reached the allowed number of retries and is now abandonning.
+batchSuccess | Indicates that the batch request was successful.
+bumpCache | When a call for an item fully loaded in the microcache succeeds, it's ttl gets extended.
+clearCache | When an item in the microcache has reached it's ttl and is now being evicted.
+
## Testing
`npm test`
-## Contributing
+## References
+
+[White paper - Microcaching]()
+
+## Contribute
+
+Please do! This is an open source project - if you see something that you want, [open an issue](//github.com/kalm/kalm.js/issues/new) or file a pull request.
+
+If you have a major change, it would be better to open an issue first so that we can talk about it.
+
+I am always looking for more maintainers, as well. Get involved.
+
+## License
-## License
+[Apache 2.0](LICENSE) (c) 2017 Frederic Charette
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..79b14b3
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,348 @@
+{
+ "name": "rest-batcher",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@sinonjs/formatio": {
+ "version": "2.0.0",
+ "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz",
+ "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==",
+ "dev": true,
+ "requires": {
+ "samsam": "1.3.0"
+ }
+ },
+ "assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "chai": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
+ "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
+ "dev": true,
+ "requires": {
+ "assertion-error": "1.1.0",
+ "check-error": "1.0.2",
+ "deep-eql": "3.0.1",
+ "get-func-name": "2.0.0",
+ "pathval": "1.1.0",
+ "type-detect": "4.0.8"
+ }
+ },
+ "check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "deep-eql": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+ "dev": true,
+ "requires": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "get-func-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "1.0.0",
+ "inflight": "1.0.6",
+ "inherits": "2.0.3",
+ "minimatch": "3.0.4",
+ "once": "1.4.0",
+ "path-is-absolute": "1.0.1"
+ }
+ },
+ "growl": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
+ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
+ "dev": true
+ },
+ "he": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "1.4.0",
+ "wrappy": "1.0.2"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+ "dev": true
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ },
+ "just-extend": {
+ "version": "1.1.27",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz",
+ "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==",
+ "dev": true
+ },
+ "lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+ "dev": true
+ },
+ "lolex": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz",
+ "integrity": "sha512-A5pN2tkFj7H0dGIAM6MFvHKMJcPnjZsOMvR7ujCjfgW5TbV6H9vb1PgxLtHvjqNZTHsUolz+6/WEO0N1xNx2ng==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "1.1.11"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "mocha": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.1.1.tgz",
+ "integrity": "sha512-kKKs/H1KrMMQIEsWNxGmb4/BGsmj0dkeyotEvbrAuQ01FcWRLssUNXCEUZk6SZtyJBi6EE7SL0zDDtItw1rGhw==",
+ "dev": true,
+ "requires": {
+ "browser-stdout": "1.3.1",
+ "commander": "2.11.0",
+ "debug": "3.1.0",
+ "diff": "3.5.0",
+ "escape-string-regexp": "1.0.5",
+ "glob": "7.1.2",
+ "growl": "1.10.3",
+ "he": "1.1.1",
+ "minimatch": "3.0.4",
+ "mkdirp": "0.5.1",
+ "supports-color": "4.4.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ },
+ "nise": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-1.3.3.tgz",
+ "integrity": "sha512-v1J/FLUB9PfGqZLGDBhQqODkbLotP0WtLo9R4EJY2PPu5f5Xg4o0rA8FDlmrjFSv9vBBKcfnOSpfYYuu5RTHqg==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/formatio": "2.0.0",
+ "just-extend": "1.1.27",
+ "lolex": "2.3.2",
+ "path-to-regexp": "1.7.0",
+ "text-encoding": "0.6.4"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1.0.2"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-to-regexp": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+ "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
+ "dev": true,
+ "requires": {
+ "isarray": "0.0.1"
+ }
+ },
+ "pathval": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+ "dev": true
+ },
+ "samsam": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz",
+ "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==",
+ "dev": true
+ },
+ "sinon": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-5.0.3.tgz",
+ "integrity": "sha512-kzBkET1Hf0r0J4uVnlicuAEiq9nnhPrEHZWS0mds+5EaB9rA0XoliIkLaqkBNU9lwPuJACo/velUQQOmTRJtUw==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/formatio": "2.0.0",
+ "diff": "3.5.0",
+ "lodash.get": "4.4.2",
+ "lolex": "2.3.2",
+ "nise": "1.3.3",
+ "supports-color": "5.4.0",
+ "type-detect": "4.0.8"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+ "dev": true,
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
+ "supports-color": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
+ "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "2.0.0"
+ }
+ },
+ "text-encoding": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
+ "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
+ "dev": true
+ },
+ "type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
index 1bc50fd..c82f020 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,18 @@
{
- "name": "rest-batcher",
+ "name": "rest-store",
"version": "1.0.0",
- "description": "A resource access indexer and batcher",
- "main": "index.js",
- "directories": {
- "test": "test"
- },
+ "description": "A data access request manager",
+ "main": "src/index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "npm run test:unit && npm run test:integration",
+ "test:unit": "mocha ./tests/unit --exit",
+ "test:integration": "mocha ./tests/integration --exit"
},
- "author": "",
- "license": "ISC"
+ "author": "frederic charette",
+ "license": "Apache 2.0",
+ "devDependencies": {
+ "chai": "^4.1.2",
+ "mocha": "^5.1.1",
+ "sinon": "^5.0.3"
+ }
}
diff --git a/src/index.js b/src/index.js
index 295c563..2c00504 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,7 +4,7 @@
/* Requires ------------------------------------------------------------------*/
-const abatch = require('./batch');
+const abatch = require('./queue');
const { requiredParam } = require('./utils');
const EventEmitter = require('events').EventEmitter;
diff --git a/src/queue.js b/src/queue.js
index 091d305..a692755 100644
--- a/src/queue.js
+++ b/src/queue.js
@@ -128,7 +128,7 @@ function queue(config, emitter) {
return `${context}::${id}`;
}
- return { add, skip, store };
+ return { add, skip, store, complete, contextKey, retry, query };
}
module.exports = batcher;
diff --git a/src/store.js b/src/store.js
index 4346137..b455909 100644
--- a/src/store.js
+++ b/src/store.js
@@ -8,9 +8,10 @@
* Store constructor
* @param {object} config The options for the store
* @param {EventEmitter} emitter The event-emitter instance for the batcher
+ * @param {Map} store A store instance to replace the default in-memory Map
*/
-function localStore(config, emitter) {
- const store = new Map();
+function localStore(config, emitter, store) {
+ store = store || new Map();
/**
* Performs a query that returns a single entities to be cached
@@ -60,26 +61,30 @@ function localStore(config, emitter) {
return store.delete(key);
}
+ /**
+ * Attempts to invalidate a key once it's cache step time expires
+ * @param {string} key The key to be evaluated for invalidation
+ */
function lru(key) {
const record = store.get(key);
if (record) {
if (record.value && record.timer) {
const now = Date.now();
if (now + config.cache.step <= record.timestamp + config.cache.ttl && record.bump === true) {
- emitter.emit('bumpCache', { key, timestamp: record.timestamp, expires: now + config.cache.step });
+ emitter.emit('cacheBump', { key, timestamp: record.timestamp, expires: now + config.cache.step });
clearTimeout(record.timer);
value.timer = setTimeout(() => clear(key), config.cache.step);
record.bump = false;
}
else {
- emitter.emit('clearCache', { key, timestamp: record.timestamp, expires: now });
+ emitter.emit('cacheClear', { key, timestamp: record.timestamp, expires: now });
clear(key);
}
}
}
}
- return { get, set, has, clear };
+ return { get, set, has, clear, lru };
}
/* Exports -------------------------------------------------------------------*/
diff --git a/test/unit/batcher.spec.js b/test/unit/batcher.spec.js
deleted file mode 100644
index e092520..0000000
--- a/test/unit/batcher.spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-const batcher = require('../../../../server/helpers/media-batcher');
-
-const { expect } = require('chai');
-const sinon = require('sinon');
-
-describe('helper/media-batcher', () => {
- const testElement = {
- id: 123,
- status: 'approved',
- };
-
- const tick = 10;
-
- describe('add', () => {
- let storeStub;
- let batcherInstance;
-
- beforeEach(() => {
- storeStub = {
- get: sinon.stub(),
- search: sinon.stub(),
- has: sinon.stub(),
- storageKey: id => `${id}`,
- };
-
- batcherInstance = batcher(storeStub, 'getOne', { tick });
- storeStub.search.returns(Promise.resolve({ [testElement.id]: testElement }));
- });
-
- afterEach(() => {
- storeStub.get = null;
- storeStub.has = null;
- storeStub.search = null;
-
- batcherInstance = null;
- });
-
- it('should be a method', () => {
- expect(batcherInstance.add).to.be.a('function');
- });
-
- it('should return a list of promises equal to the number of ids provided', () => {
- expect(batcherInstance.add([123, 456, 789])).to.have.length(3);
- });
-
- it('should queue all requested ids with the requested options', (done) => {
- storeStub.has.returns(false);
- batcherInstance.add([123, 456, 789], { foo: 'bar' });
-
- expect(storeStub.search.called).to.be.false;
-
- setTimeout(() => {
- expect(storeStub.search.calledWith({ ids: [123, 456, 789], foo: 'bar' }, 'getOne')).to.be.true;
- done();
- }, tick + 1);
- });
-
- it('should resolve items already in the store', (done) => {
- storeStub.has.returns(true);
- storeStub.get.returns(Promise.resolve(testElement));
- const add = batcherInstance.add([123]);
-
- expect(storeStub.search.notCalled).to.be.true;
-
- setTimeout(() => {
- expect(storeStub.search.notCalled).to.be.true;
- expect(Promise.all(add)).to.eventually.deep.equal([testElement]).notify(done);
- }, tick + 1);
- });
- });
-});
diff --git a/test/unit/store.spec.js b/test/unit/store.spec.js
deleted file mode 100644
index 5463686..0000000
--- a/test/unit/store.spec.js
+++ /dev/null
@@ -1,216 +0,0 @@
-const astore = require('../src');
-const expect = require('chai').expect;
-
-const testDuration = 100;
-
-const testDao = {
- getOne: (opts) => {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve({
- id: opts.id,
- stuff: 'yes',
- })
- }, testDuration);
- });
- },
- search: (opts) => {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve([
- {
- id: 123,
- stuff: 'yes',
- },
- {
- id: 456,
- stuff: 'yes',
- },
- ])
- }, testDuration);
- });
- },
- searchIndexed: (opts) => {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve({
- '123': {
- id: 123,
- stuff: 'yes',
- },
- '456': {
- id: 456,
- stuff: 'yes',
- },
- })
- }, testDuration);
- });
- },
- searchIndexedNull: (opts) => {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve({
- '123': {
- id: 123,
- stuff: 'yes',
- },
- '456': null,
- })
- }, testDuration);
- });
- },
-};
-
-describe('Smoke test, single', () => {
-
- let testStore;
- beforeEach(() => {
- testStore = astore(testDao);
- });
-
- it('should cache entities', (done) => {
- let now = Date.now();
- testStore.get({ id: 123 }, 'getOne')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.get.bind(null, { id: 123 }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.most(1);
- now = Date.now();
- done();
- });
- });
-});
-
-describe('Smoke test, single - disabled', () => {
-
- let testStore;
- beforeEach(() => {
- testStore = astore(testDao, { enabled: false });
- });
-
- it('should make a direct calls', (done) => {
- let now = Date.now();
- testStore.get({ id: 123 }, 'getOne')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.get.bind(null, { id: 123 }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- done();
- });
- });
-});
-
-describe('Smoke test, list', () => {
-
- let testStore;
- beforeEach(() => {
- testStore = astore(testDao);
- });
-
- it('should cache entities individually', (done) => {
- let now = Date.now();
- testStore.getList({ ids: [123, 456] }, 'getOne')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.getList.bind(null, { ids: [123, 456] }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.most(1);
- now = Date.now();
- done();
- });
- });
-});
-
-
-describe('Smoke test, search', () => {
-
- let testStore;
- beforeEach(() => {
- testStore = astore(testDao);
- });
-
- it('should cache entities', (done) => {
- let now = Date.now();
- testStore.search({ }, 'search')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.get.bind(null, { id: 123 }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.most(1);
- now = Date.now();
- done();
- });
- });
-});
-
-describe('Smoke test, search indexed', () => {
-
- let testStore;
- beforeEach(() => {
- testStore = astore(testDao);
- });
-
- it('should cache entities', (done) => {
- let now = Date.now();
- testStore.search({ }, 'searchIndexed')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.get.bind(null, { id: 123 }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.most(1);
- now = Date.now();
- done();
- });
- });
-
- it('should handle null entities', (done) => {
- let now = Date.now();
- testStore.search({ }, 'searchIndexedNull')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.get.bind(null, { id: 123 }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.most(1);
- now = Date.now();
- done();
- });
- });
-});
-
-describe('Smoke test, direct', () => {
-
- let testStore;
- beforeEach(() => {
- testStore = astore(testDao);
- });
-
- it('should cache entities', (done) => {
- let now = Date.now();
- testStore.direct({ id: 123 }, 'getOne')
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- })
- .then(testStore.direct.bind(null, { id: 123 }, 'getOne'))
- .then(() => {
- expect((Date.now() - now)).to.be.at.least(testDuration - 1);
- now = Date.now();
- done();
- });
- });
-});
\ No newline at end of file
diff --git a/test/integration/dao.js b/tests/integration/dao.js
similarity index 100%
rename from test/integration/dao.js
rename to tests/integration/dao.js
diff --git a/test/integration/index.js b/tests/integration/index.js
similarity index 92%
rename from test/integration/index.js
rename to tests/integration/index.js
index 7b8d637..11b2859 100644
--- a/test/integration/index.js
+++ b/tests/integration/index.js
@@ -2,13 +2,14 @@ const expect = require('chai').expect;
const dao = require('./dao');
const store = require('../../src');
-
+// Variables:
+// Cache (true/false)
+// Batch (true/false)
+// Retry (true/false)
describe('Rest-store', () => {
describe('Happy responses', () => {
let testStore;
afterEach(() => testStore = null);
- context('Cache: true', () => {})
- context('')
beforeEach(() => {
testStore = store({
getter: {
@@ -16,8 +17,6 @@ describe('Rest-store', () => {
}
});
});
-
-
});
describe('Empty responses', () => {
diff --git a/tests/unit/queue.spec.js b/tests/unit/queue.spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/store.spec.js b/tests/unit/store.spec.js
new file mode 100644
index 0000000..fe21d0c
--- /dev/null
+++ b/tests/unit/store.spec.js
@@ -0,0 +1,138 @@
+/**
+ * Store component unit tests
+ */
+
+/* Requires ------------------------------------------------------------------*/
+
+const store = require('../../src/store');
+const EventEmitter = require('events').EventEmitter;
+const expect = require('chai').expect;
+const sinon = require('sinon');
+
+/* Local variables -----------------------------------------------------------*/
+
+const testDuration = 100;
+const config = {
+ cache: {
+ step: 10,
+ ttl: 100,
+ },
+};
+
+/* Tests ---------------------------------------------------------------------*/
+
+describe('store', () => {
+
+ let testStore;
+ let mapStub;
+ let emitterStub;
+
+ describe('#get', () => {
+ beforeEach(() => {
+ mapStub = sinon.stub(new Map());
+ emitterStub = sinon.stub(new EventEmitter());
+ testStore = store(config, emitterStub, mapStub);
+
+ const valueRecord = { value: 'foo' };
+ const promiseRecord = { promise: new Promise(()=>{}) };
+ testStore.set('testValue', valueRecord);
+ testStore.set('testPromise', promiseRecord);
+ });
+
+ it('should return a value if there\'s a value saved', () => {
+ expect(testStore.get('testValue')).to.equal('foo');
+ expect(mapStub.get).calledWith('testValue');
+ expect(valueRecord.bump).to.be.true;
+ });
+
+ it('should return a promise if there\'s a promise saved', () => {
+ expect(testStore.get('testPromise')).to.be.instanceOf(Promise);
+ expect(mapStub.get).calledWith('testPromise');
+ expect(promiseRecord.bump).to.not.be.true;
+ });
+
+ it('should return a null if the key is missing', () => {
+ expect(testStore.get('test')).to.be.null;
+ expect(mapStub.get).calledWith('test');
+ });
+ });
+
+ describe('#set', () => {
+ beforeEach(() => {
+ mapStub = sinon.stub(new Map());
+ emitterStub = sinon.stub(new EventEmitter());
+ testStore = store(config, emitterStub, mapStub);
+ });
+
+ it('should save to the store and not schedule lru', (done) => {
+ const lruMock = sinon.mock(testStore, 'lru');
+ expect(testStore.set('testValue', { value: 'foo' })).to.equal({ value: 'foo' });
+ setTimeout(() => {
+ expect(lruMock.neverCalled);
+ done();
+ }, 11);
+ });
+
+ it('should save to the store and schedule lru when there\'s a ttl', (done) => {
+ const lruMock = sinon.mock(testStore, 'lru');
+ expect(testStore.set('testLRUValue', { value: 'foo' }, { ttl: 10 })).to.equal({ value: 'foo' });
+ setTimeout(() => {
+ expect(lruMock.calledWith('testLRUValue'));
+ done();
+ }, 11);
+ });
+ });
+
+ describe('#has', () => {
+ beforeEach(() => {
+ mapStub = sinon.stub(new Map());
+ emitterStub = sinon.stub(new EventEmitter());
+ testStore = store(config, emitterStub, mapStub);
+
+ const valueRecord = { value: 'foo' };
+ const promiseRecord = { promise: new Promise(()=>{}) };
+ testStore.set('testValue', valueRecord);
+ testStore.set('testPromise', promiseRecord);
+ });
+
+ it('should return true if there\'s a value saved', () => {
+ expect(testStore.has('testValue')).to.be.true;
+ expect(mapStub.has).calledWith('testValue');
+ });
+
+ it('should return true if there\'s a promise saved', () => {
+ expect(testStore.has('testPromise')).to.be.true;
+ expect(mapStub.has).calledWith('testPromise');
+ });
+
+ it('should return false if the key is missing', () => {
+ expect(testStore.has('test')).to.be.false;
+ expect(mapStub.has).calledWith('test');
+ });
+ });
+
+ describe('#clear', () => {
+ beforeEach(() => {
+ mapStub = sinon.stub(new Map());
+ emitterStub = sinon.stub(new EventEmitter());
+ testStore = store(config, emitterStub, mapStub);
+
+ const promiseRecord = { promise: new Promise(()=>{}) };
+ testStore.set('testPromise', promiseRecord);
+ });
+
+ it('should clear the record and return true if there\'s a promise saved', () => {
+ expect(testStore.clear('testPromise')).to.be.true;
+ expect(mapStub.clear).calledWith('testPromise');
+ });
+
+ it('should do nothing and return false if the key is missing', () => {
+ expect(testStore.clear('testPromise')).to.be.false;
+ expect(mapStub.clear).calledWith('testPromise');
+ });
+ });
+
+ describe('#lru', () => {
+
+ });
+});