-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(quoteCombine): debounce and combine multiple quote calls
- Loading branch information
Showing
6 changed files
with
550 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
Oops, something went wrong.