Skip to content

Commit

Permalink
feat(quoteCombine): debounce and combine multiple quote calls
Browse files Browse the repository at this point in the history
  • Loading branch information
gadicc committed Mar 11, 2021
1 parent ac389fe commit 95bf404
Show file tree
Hide file tree
Showing 6 changed files with 550 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

1. [Common Options](#common-options)
1. [Modules](#modules)
1. [Other Methods](#other)
1. [Error Handling](#error-handling)
1. [Validation](./validation.md)

Expand Down Expand Up @@ -35,6 +36,11 @@ const result = await yahooFinance.module(query, queryOpts, moduleOpts);
1. [recommendationsBySymbol](./modules/recommendationsBySymbol.md) - similar symbols.
1. [trendingSymbols](./modules/trendingSymbols.md) - symbols trending in a country.

<a name="other"></a>
## Other Methods

1. [quoteCombine](./other/quoteCombine.md) - debounce and combine multiple quote calls.

<a name="error-handling"></a>
## Error Handling

Expand Down
33 changes: 33 additions & 0 deletions docs/other/quoteCombine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# quoteCombine

This utility function will debounce multiple calls and combine them into a
single [quote()](../modules/quote.md) call, i.e. you'll call `quoteCombine()`
many times, and 50ms after the last call, `quote()` will be called once so
that only a single HTTP request is sent to collect the data for all symbols.

## Usage:

```js
import yahooFinance from 'yahoo-finance2';

// Only a single HTTP request will be made for all of these.
databaseResults.forEach(async (row) => {
const result = await yahooFinance.quoteCombine(row.symbol);
// do something
});
```

Notes:

* Each `quoteCombine()` call receives the result for only the symbol it
asked for.

* Query options and the shape of the return result is identical to that of
[quote()](../modules/quote.md).

* If you call `quoteCombine()` multiple times with different `queryOptions`,
`quote()` will be called separately for each unique set of `queryOptions`
and its associated set of symbols.

* It's fine if your code calls `quoteCombine()` many times for the same
symbol. The symbol will be queried only once, and returned many times.
36 changes: 36 additions & 0 deletions src/other/quoteCombine.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import quoteCombine from "./quoteCombine";
import testYf from "../../tests/testYf";

const yf = testYf({ quoteCombine });

jest.useFakeTimers();

describe("quoteCombine", () => {
it("works with a single result", (done) => {
const devel = "quoteCombine-AAPL.json";
yf.quoteCombine("AAPL", undefined, { devel })
.then((result: any) => {
expect(result.symbol).toBe("AAPL");
done();
})
.catch(done);
jest.runAllTimers();
});

it("works with two results", (done) => {
const opts = { devel: "quoteCombine-AAPL-TSLA.json" };
Promise.all([
yf.quoteCombine("AAPL", undefined, opts).then((result: any) => {
expect(result.symbol).toBe("AAPL");
}),

yf.quoteCombine("TSLA", undefined, opts).then((result: any) => {
expect(result.symbol).toBe("TSLA");
}),
])
.then(() => done())
.catch(done);

jest.runAllTimers();
});
});
96 changes: 96 additions & 0 deletions src/other/quoteCombine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type {
ModuleOptions,
ModuleOptionsWithValidateTrue,
ModuleOptionsWithValidateFalse,
ModuleThis,
} from "../lib/moduleCommon";

import type { QuoteOptions, Quote } from "../modules/quote";
import quote from "../modules/quote";

import validateAndCoerceTypes from "../lib/validateAndCoerceTypes";

const DEBOUNCE_TIME = 50;

const slugMap = new Map();

export default function quoteCombine(
this: ModuleThis,
query: string,
queryOptionsOverrides?: QuoteOptions,
moduleOptions?: ModuleOptionsWithValidateTrue
): Promise<Quote>;

export default function quoteCombine(
this: ModuleThis,
query: string,
queryOptionsOverrides?: QuoteOptions,
moduleOptions?: ModuleOptionsWithValidateFalse
): Promise<any>;

export default function quoteCombine(
this: ModuleThis,
query: string,
queryOptionsOverrides: QuoteOptions = {},
moduleOptions?: ModuleOptions
): Promise<any> {
const symbol = query;

if (typeof symbol !== "string")
throw new Error(
"quoteCombine expects a string query parameter, received: " +
JSON.stringify(symbol, null, 2)
);

validateAndCoerceTypes({
source: "quoteCombine",
type: "options",
object: queryOptionsOverrides,
schemaKey: "#/definitions/QuoteOptions",
options: this._opts.validation,
});

// Make sure we only combine requests with same options
const slug = JSON.stringify(queryOptionsOverrides);

let entry = slugMap.get(slug);
if (!entry) {
entry = {
timeout: null,
queryOptionsOverrides,
symbols: new Map(),
};
slugMap.set(slug, entry);
}

if (entry.timeout) clearTimeout(entry.timeout);

const thisQuote = quote.bind(this);

return new Promise((resolve, reject) => {
let symbolPromiseCallbacks = entry.symbols.get(symbol);
if (!symbolPromiseCallbacks) {
symbolPromiseCallbacks = [];
entry.symbols.set(symbol, symbolPromiseCallbacks);
}

symbolPromiseCallbacks.push({ resolve, reject });

entry.timeout = setTimeout(() => {
slugMap.delete(slug);

const symbols: string[] = Array.from(entry.symbols.keys());
// @ts-ignore
thisQuote(symbols, queryOptionsOverrides, moduleOptions)
.then((results) => {
for (let result of results) {
const symbol = result.symbol;
entry.symbols.get(symbol).forEach((p: any) => p.resolve(result));
}
})
.catch((error) => {
for (let promise of entry.symbols.values()) promise.reject(error);
});
}, DEBOUNCE_TIME);
});
}
Loading

0 comments on commit 95bf404

Please sign in to comment.