Skip to content

Commit

Permalink
Merge pull request #107 from fed135/next
Browse files Browse the repository at this point in the history
v3.0.0 official release
  • Loading branch information
fed135 authored Jul 12, 2021
2 parents 3d57bea + fc194cd commit 9eaf8c8
Show file tree
Hide file tree
Showing 22 changed files with 442 additions and 523 deletions.
8 changes: 7 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 9
"ecmaVersion": 11
},
"rules": {
"arrow-body-style": "off",
Expand Down Expand Up @@ -62,5 +63,10 @@
],
"linebreak-style": "off",
"no-lonely-if": "off"
},
"env": {
"node": true,
"mocha": true,
"es6": true
}
}
30 changes: 30 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: pr-validation

on:
pull_request:
branches:
- '*'

jobs:
build:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x, 16.x]

steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm install
npm run lint
npm run test
wget --no-check-certificate --content-disposition https://gist.githubusercontent.com/fed135/56282783a4c13d87a7f89c178b5d08d7/raw/bacd6f814475726cee37a5a591cb313dd1542d2b/sample.txt && npm run bench
env:
CI: true
16 changes: 0 additions & 16 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2019 Frederic Charette
Copyright 2021 Frederic Charette

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<br/>

[![ha-store](https://img.shields.io/npm/v/ha-store.svg)](https://www.npmjs.com/package/ha-store)
[![Node](https://img.shields.io/badge/node->%3D8.0-blue.svg)](https://nodejs.org)
[![Node](https://img.shields.io/badge/node->%3D14.0-blue.svg)](https://nodejs.org)
[![Build Status](https://travis-ci.org/fed135/ha-store.svg?branch=master)](https://travis-ci.org/fed135/ha-store)
[![Dependencies Status](https://david-dm.org/fed135/ha-store.svg)](https://david-dm.org/fed135/ha-store)

Expand Down Expand Up @@ -36,29 +36,28 @@ Learn how you can improve your app's performance, design and resiliancy [here](h
const store = require('ha-store');
const itemStore = store({
resolver: getItems,
uniqueParams: ['language']
delimiter: ['language']
});

// Define your resolver
function getItem(ids, params, contexts) {
function getItems(ids, params, contexts) {
// Ids will be a list of all the unique requested items
// Params will be the parameters for the request, which must be declared in the `uniqueParams` config of the store
// Params will be the parameters for the request, which must be declared in the `delimiter` config of the store
// Contexts will be the list of originating context information

// Now perform some exensive network call or database lookup...

// Then, respond with your data formatted into one of these two formats:
// a) [ { id: '123', language: 'fr', name: 'fred' } ]
// b) { '123': { language: 'fr', name: 'fred' } }
// Then, respond with your data formatted into this formats:
// { '123': { language: 'fr', name: 'fred' } }
}

// Now to use your store
itemStore.get('123', { language: 'fr' }, { some: 'context' })
itemStore.get('123', { language: 'fr' }, { requestId: '123' })
.then(item => /* The item you requested */);

// You can even ask for more than one item at a time
itemStore.get(['123', '456'], { language: 'en' }, { another: 'context' })
.then(items => /* All the items you requested */);
itemStore.getMany(['123', '456'], { language: 'en' }, { requestId: '123' })
.then(items => /* All the items you requested, in Promise.allSettled fashion */);
```


Expand All @@ -67,11 +66,10 @@ itemStore.get(['123', '456'], { language: 'en' }, { another: 'context' })
Name | Required | Default | Description
--- | --- | --- | ---
resolver | true | - | The method to wrap, and how to interpret the returned data. Uses the format `<function(ids, params)>`
responseParser | false | (system) | The method that format the results from the resolver into an indexed collection. Accepts indexed collections or arrays of objects with an `id` property. Uses the format `<function(response, requestedIds, params)>`
uniqueParams | false | `[]` | The list of parameters that, when passed, generate unique results. Ex: 'language', 'view', 'fields', 'country'. These will generate different combinations of cache keys.
delimiter | false | `[]` | The list of parameters that, when passed, generate unique results. Ex: 'language', 'view', 'fields', 'country'. These will generate different combinations of cache keys.
store | false | `null` | A custom store for the data, like [ha-store-redis](https://github.com/fed135/ha-redis-adapter).
cache | false | <pre>{&#13;&#10;&nbsp;&nbsp;limit: 5000,&#13;&#10;&nbsp;&nbsp;ttl: 300000&#13;&#10;}</pre> | Caching options for the data - `limit` - the maximum number of records, and `ttl` - time to live for a record.
batch | false | <pre>{&#13;&#10;&nbsp;&nbsp;tick: 50,&#13;&#10;&nbsp;&nbsp;max: 100&#13;&#10;}</pre> | Batching options for the requests
cache | false | <pre>{&#13;&#10;&nbsp;&nbsp;limit: 5000,&#13;&#10;&nbsp;&nbsp;ttl: 300000&#13;&#10;}</pre> | Caching options for the data - `limit` - the maximum number of records, and `ttl` - time to live for a record in milliseconds.
batch | false | <pre>{&#13;&#10;&nbsp;&nbsp;delay: 50,&#13;&#10;&nbsp;&nbsp;limit: 100&#13;&#10;}</pre> | Batching options for the requests

*All options are in (ms)

Expand Down Expand Up @@ -112,5 +110,5 @@ I am always looking for more maintainers, as well.

## License

[Apache 2.0](LICENSE) (c) 2019 Frederic Charette
[Apache 2.0](LICENSE) (c) 2021 Frederic Charette

27 changes: 11 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
{
"name": "ha-store",
"version": "2.0.5",
"version": "3.0.0",
"description": "Efficient data fetching",
"main": "src/index.js",
"scripts": {
"lint": "eslint .",
"lint:fix": "yarn lint --fix",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "mocha ./tests/unit --exit",
"test:integration": "mocha ./tests/integration --exit",
"bench": "node ./tests/profiling/index.js"
},
"engines": {
"node": ">=8.0.0"
"node": ">=14.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fed135/ha-store.git"
},
"keywords": [
"store",
"high",
"availability",
"network",
"optimize",
"throughput",
"retry",
"request",
"cache",
"service",
"batch",
"micro",
"latency",
"congestion",
"control",
"tlru"
],
"bugs": {
Expand All @@ -40,12 +35,12 @@
"author": "frederic charette <[email protected]>",
"license": "Apache-2.0",
"devDependencies": {
"chai": "^4.2.0",
"heapdump": "^0.3.12",
"mocha": "^6.0.0",
"sinon": "^7.2.0",
"split2": "^3.1.1",
"ha-store-redis": "^2.0.1"
"chai": "^4.3.0",
"eslint": "^7.29.0",
"ha-store-redis": "^2.0.1",
"mocha": "^9.0.0",
"sinon": "^11.1.0",
"split2": "^3.2.0"
},
"contributors": [
"frederic charette <[email protected]>",
Expand All @@ -54,6 +49,6 @@
],
"typings": "./src/index.d.ts",
"dependencies": {
"lru-native2": "^1.2.0"
"lru-native2": "^1.2.5"
}
}
38 changes: 17 additions & 21 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,39 @@ type Params = {
[key: string]: string
}

type RequestIds = string | number | string[] | number[]
type Serializable = string | number | boolean | { [key: string]: Serializable } | Array<Serializable>

export interface HAExternalStore {
get: (key: string) => Promise<Serializable>
getMulti: (recordKey: (contextKey: string) => string, keys: RequestIds) => Promise<Serializable[]>
set: (recordKey: (contextKey: string) => string, keys: RequestIds, values: Serializable) => Promise<Serializable>
clear: (key?: string) => boolean
size: () => number
get<Response>(key: string): Promise<Response>
getMulti<Response>(recordKey: (contextKey: string) => string, keys: RequestIds): Promise<Response[]>
set<DataType>(recordKey: (contextKey: string) => string, keys: RequestIds, values: DataType): boolean
clear(key?: string): boolean
size(): number
connection?: any
}

export interface HAStoreConfig {
resolver(ids: RequestIds, params?: Params, context?: Serializable): Promise<Serializable>
uniqueParams?: string[]
responseParser?(
response: Serializable,
requestedIds: string[] | number[],
params?: Params
): object
resolver<Response>(ids: string[], params?: Params): Promise<{ [id: string]: Response }>
resolver<Response, Context>(ids: string[], params?: Params, context?: Context): Promise<{ [id: string]: Response }>
delimiter?: string[]
cache?: {
limit?: number
ttl?: number
}
batch?: {
tick?: number
max?: number
delay?: number
limit?: number
},
store?: HAExternalStore
}

export interface HAStore extends EventEmitter {
get(ids: string | number, params?: Params, context?: Serializable): Promise<Serializable>
set(items: Serializable, ids: string[] | number[], params?: Params): Promise<Serializable>
clear(ids: RequestIds, params?: Params): void
get<Response>(id: string, params?: Params): Promise<Response>
get<Response, Context>(id: string, params?: Params, context?: Context): Promise<Response>
getMany<Response>(id: string[], params?: Params): Promise<{status: string, value: Response}[]>
getMany<Response, Context>(id: string[], params?: Params, context?: Context): Promise<{status: string, value: Response}[]>
set(items: { [id: string]: any }, ids: string[], params?: Params): boolean
clear(ids: string[], params?: Params): void
size(): { contexts: number, queries: number, records: number }
getKey(id: string | number, params?: Params): string
getStorageKey(id: string, params?: Params): string
}

export default function batcher(config: HAStoreConfig, emitter?: EventEmitter): HAStore
72 changes: 21 additions & 51 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
/**
* Batcher index
*/

'use strict';

/* Requires ------------------------------------------------------------------*/

const queue = require('./queries.js');
const {contextKey, recordKey, contextRecordKey} = require('./utils.js');
const EventEmitter = require('events').EventEmitter;
Expand All @@ -17,18 +9,16 @@ class HaStore extends EventEmitter {
constructor(initialConfig, emitter) {
super();

// Parameter validation
if (typeof initialConfig.resolver !== 'function') {
throw new Error(`config.resolver [${initialConfig.resolver}] is not a function`);
}

if (emitter && !emitter.emit) {
if (!emitter?.emit) {
throw new Error(`${emitter} is not an EventEmitter`);
}

this.config = hydrateConfig(initialConfig);

// Local variables
if (this.setMaxListeners) {
this.setMaxListeners(Infinity);
}
Expand All @@ -38,72 +28,52 @@ class HaStore extends EventEmitter {
this.queue = queue(
this.config,
this,
this.store,
this.store
);
}

/**
* Gets a list of records from source
* @param {string|number|array<string|number>} ids The id of the record to fetch
* @param {object} params (Optional)The Request parameters
* @returns {Promise} The eventual single record
*/
async get(ids, params = {}, agg = null) {
get(id, params = {}, agg = null) {
if (params === null) params = {};
const key = contextKey(this.config.delimiter, params);
return this.queue.getHandles(key, [id], params, agg)
.then(handles => handles[0]);
}

getMany(ids, params = {}, agg = null) {
if (params === null) params = {};
const requestIds = (Array.isArray(ids)) ? ids : [ids];
const key = contextKey(this.config.uniqueParams, params);
const handles = await this.queue.getHandles(key, requestIds, params, agg);
return Promise.all(handles)
.then(response => (!Array.isArray(ids)) ? response[0] : response);
const key = contextKey(this.config.delimiter, params);
return this.queue.getHandles(key, ids, params, agg)
.then((handles) => Promise.allSettled(handles)
.then((outcomes) => ids.reduce((handles, id, index) => {
handles[id] = outcomes[index];
return handles;
}, {})));
}

/**
* Inserts results into cache manually
* @param {*} items Raw results from a data-source to load into cache
* @param {array<string|number>} ids The id(s) to extract from the raw dataset
* @param {object} params (Optional)The Request parameters
* @returns {Promise} The eventual single record
*/
set(items, ids, params = {}) {
if (!Array.isArray(ids) || ids.length === 0) throw new Error('Missing required argument id list in batcher #set. ');
const key = contextKey(this.config.uniqueParams, params);
const key = contextKey(this.config.delimiter, params);
return this.store.set(contextRecordKey(key), ids, items);
}

/**
* Clears one or more recors from temp store
* @param {string|number|array<string|number>} ids The id(s) to clear
* @param {object} params (Optional) The Request parameters
* @returns {boolean} The result of the clearing
*/
clear(ids, params) {
if (this.store === null) return true;
if (Array.isArray(ids)) {
return ids.map(id => this.clear(id, params));
}

return this.store.clear(this.getKey(ids, params));
return this.store.clear(this.getStorageKey(ids, params));
}

/**
* Returns the amount of records and contexts in memory
* @returns {object}
*/
size() {
return {
...this.queue.size(),
records: (this.store) ? this.store.size() : 0,
};
}

/**
* Returns a record key
* @param {string|number} id The id of the item
* @param {object} params The parameters for the request
* @returns {string} The record key
*/
getKey(id, params) {
return recordKey(contextKey(this.config.uniqueParams, params), id);
getStorageKey(id, params) {
return recordKey(contextKey(this.config.delimiter, params), id);
}
}

Expand Down
Loading

0 comments on commit 9eaf8c8

Please sign in to comment.