├── .gitignore
├── .npmignore
├── .prettierrc
├── CONTRIBUTE.md
├── LICENSE
├── NOTE.md
├── README.md
├── docs
├── .gitignore
├── doczrc.js
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── PromisePlayground.tsx
│ ├── SnapshotPlayground.tsx
│ ├── SubscriptionPlayground.tsx
│ ├── gatsby-theme-docz
│ │ └── wrapper.tsx
│ ├── index.mdx
│ ├── promise-playground.mdx
│ ├── snapshot-playground.mdx
│ ├── subscription-playground.mdx
│ └── usage.mdx
├── tsconfig.json
└── yarn.lock
├── examples
└── nodejs-candles.cjs
├── package.json
├── rollup.config.js
├── src
├── __mocks__
│ └── endpoint.ts
├── config.ts
├── endpoint.ts
├── eventFlags.ts
├── feed.test.ts
├── feed.ts
├── index.ts
├── interfaces.ts
├── subscriptions.ts
├── timeSeriesAggregator.ts
└── utils.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Meta
2 | **/.DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-error.log
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # node-waf configuration
28 | .lock-wscript
29 |
30 | # Compiled binary addons (http://nodejs.org/api/addons.html)
31 | build/Release
32 |
33 | # Dependency directories
34 | node_modules
35 | jspm_packages
36 |
37 | # Optional npm cache directory
38 | .npm
39 |
40 | # Optional REPL history
41 | .node_repl_history
42 |
43 | # Distribution
44 | dist
45 |
46 | # Temp env
47 | .env
48 |
49 | .npmrc
50 |
51 | /lib
52 |
53 | #IDE
54 | .idea
55 | .vscode
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | docs
3 | examples
4 | tsconfig.json
5 | tslint.json
6 | yarn.lock
7 | .prettierrc
8 | .idea
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "trailingComma": "all",
4 | "singleQuote": true
5 | }
--------------------------------------------------------------------------------
/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | # Contributing to js-api
2 |
3 | We would be glad if you contribute to our source code and help us to improve js-api even more! We propose you to follow these guidelines:
4 | ## Contributing a Patch
5 |
6 | 1. Create your change to the repo in question.
7 | - Fork the desired repo, develop and test your code changes.
8 | - Ensure that your code is clear and comprehensible.
9 | - Ensure that your code has an appropriate set of unit tests which all pass.
10 |
11 | 2. Submit a pull request.
12 | 3. The repo owner will review your request. If it is approved, the change will be merged. If it needs additional work, the repo owner will respond with useful comments.
13 |
14 | ## Question or problem
15 | If you have any questions about how to use js-api, please contact via Github issues. Just create an issue with a question.
16 |
17 | ## Submitting an issue
18 | You can help us by submitting an issue to our GitHub Repository in case you find a bug in the source code, a mistake in the documentation or a proposal. A Pull Request with a fix would be even better. Before you submit an issue, search the archive, maybe your question was already answered.
19 |
20 | Here are the submission guidelines we would like you to follow:
21 | * Detailed overview of the Issue
22 | * Related issues - has a similar issue been reported before?
23 | * Reproduce the Error - A set of steps to reproduce the error (in case of a bug)
24 | * Suggest a Fix - if you can't fix the bug yourself, perhaps you can point to what might be causing the problem (line of code or commit)
25 |
26 | ## Documentation fixes
27 | If you have any ideas how to help us to improve the documentation, please let others know what you're working on by creating a new issue or comment on a related existing one (please, follow the guidelines contained in Submitting an issue).
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/NOTE.md:
--------------------------------------------------------------------------------
1 | Copyright ©2000 Devexperts LLC. All rights reserved.
2 |
3 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
4 |
5 | If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @dxfeed/api [](https://www.npmjs.com/package/@dxfeed/api)
2 | ## IMPORTANT: This API is outdated, please use [DXLINK API](https://github.com/dxFeed/dxLink)
3 |
4 | This package provides access to [dxFeed](https://www.dxfeed.com/) streaming data.
5 |
6 | Our package is easy to integrate with any modern framework.
7 |
8 | ## Install
9 |
10 | ```sh
11 | npm install @dxfeed/api
12 | ```
13 |
14 | ## NodeJS usage
15 |
16 | Install `cometd-nodejs-client` package
17 |
18 | ```sh
19 | npm install cometd-nodejs-client
20 | ```
21 |
22 | and use it in your code
23 |
24 | ```js
25 | require('cometd-nodejs-client').adapt()
26 | // or
27 | import * as CometdNodejsClient from 'cometd-nodejs-client'
28 | CometdNodejsClient.adapt()
29 | ```
30 |
31 | ## Basic Usage
32 |
33 | We have several classes in implementation:
34 |
35 | - Feed **_(public)_**
36 | - Endpoint **_(private)_**
37 | - Subscriptions **_(private)_**
38 |
39 | The _Feed_ is entry point for configuration and creating subscriptions.
40 | _Feed_ manages private classes for connecting and subscribing.
41 | The _Endpoint_ is responsible for managing the web socket connection.
42 | _Subscriptions_ for managing open subscriptions.
43 |
44 | ## Import package
45 |
46 | ```ts
47 | import Feed from '@dxfeed/api'
48 | ```
49 |
50 | ## Configure & Create connection
51 |
52 | Create instance of Feed.
53 |
54 | ```ts
55 | const feed = new Feed()
56 | ```
57 |
58 | Provide auth token if needed.
59 |
60 | ```ts
61 | feed.setAuthToken('authToken')
62 | ```
63 |
64 | Set web socket address and open connection.
65 |
66 | ```ts
67 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
68 | ```
69 |
70 | ## Configure & Create subscription
71 |
72 | You should specify event types and symbol names.
73 |
74 | ```ts
75 | feed.subscribe<{ value: number }>(
76 | [EventType.Summary, EventType.Trade] /* event types */,
77 | ['AEX.IND:TEI'] /* symbols */,
78 | handleEvent
79 | )
80 | ```
81 |
82 | For timed subscription you should also provide time to start subscription from.
83 |
84 | For Candle event type along with base symbol, you should specify an aggregation period. You can also set price type. More details: [https://kb.dxfeed.com/en/data-access/rest-api.html#candle-symbols](https://kb.dxfeed.com/en/data-access/rest-api.html#candle-symbols)
85 |
86 | ```ts
87 | feed.subscribeTimeSeries<{ value: number }>(
88 | [EventType.Summary, EventType.Trade] /* event types */,
89 | ['AEX.IND:TEI'] /* symbols */,
90 | 0 /* fromTime */,
91 | handleEvent
92 | )
93 | ```
94 |
95 | Last argument its event handler for process incoming events.
96 |
97 | ```ts
98 | const handleEvent = (event) => {
99 | /* process event */
100 | }
101 | ```
102 |
103 | ## Close subscription
104 |
105 | All subscribe methods return unsubscribe handler, you need to call this method for unsubscribe.
106 |
107 | ```ts
108 | const unsubscribe = feed.subscribe(eventTypes, symbols, handleEvent)
109 |
110 | onExit(() => unsubscribe())
111 | ```
112 |
113 | ## Aggregated API
114 |
115 | ### Get TimeSeries
116 |
117 | If you want to get TimeSeries events for a given time period, refer to example below.
118 |
119 | ```ts
120 | // inside async function
121 | const events = await feed.getTimeSeries(
122 | 'AAPL{=15m}',
123 | EventType.Candle,
124 | fromDate.getTime(),
125 | toDate.getTime()
126 | )
127 | ```
128 |
129 | ### Subscribe TimeSeries snapshot
130 |
131 | If you want to subscribe to TimeSeries events, refer to example below.
132 |
133 | ```ts
134 | const unsubscribe = feed.subscribeTimeSeriesSnapshot('AAPL{=15m}', EventType.Candle, (candles) => {
135 | // process candles
136 | chart.setCandles(candles)
137 | })
138 | ```
139 |
140 | ## Close connection
141 |
142 | If you need to close the web socket connection
143 |
144 | ```ts
145 | feed.disconnect()
146 | ```
147 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Meta
2 | **/.DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-error.log
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # node-waf configuration
28 | .lock-wscript
29 |
30 | # Compiled binary addons (http://nodejs.org/api/addons.html)
31 | build/Release
32 |
33 | # Dependency directories
34 | node_modules
35 | jspm_packages
36 |
37 | # Optional npm cache directory
38 | .npm
39 |
40 | # Optional REPL history
41 | .node_repl_history
42 |
43 | # Distribution
44 | dist
45 |
46 | # Temp env
47 | .env
48 |
49 | .npmrc
50 |
51 | .docz
52 |
--------------------------------------------------------------------------------
/docs/doczrc.js:
--------------------------------------------------------------------------------
1 | export default {
2 | typescript: true,
3 | dest: '/dist',
4 | menu: [
5 | 'Getting Started',
6 | 'Basic Usage',
7 | 'Playground',
8 | ],
9 | title: '@dxfeed/api',
10 | description: 'This package provides access to dxFeed streaming data. Easy integrates with any modern framework.',
11 | repository: 'https://github.com/dxfeed/dxfeed-js-api',
12 | }
13 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dxfeed/api-docs",
3 | "version": "1.0.0",
4 | "license": "MPL-2.0",
5 | "repository": "https://github.com/dxFeed/dxfeed-js-api.git",
6 | "private": true,
7 | "scripts": {
8 | "start": "docz dev",
9 | "build": "docz build --base=/dxfeed-js-api/",
10 | "deploy": "gh-pages -d dist"
11 | },
12 | "dependencies": {
13 | "@emotion/core": "^10.0.14",
14 | "@emotion/styled": "^10.0.14",
15 | "@material-ui/core": "^4.11.0",
16 | "docz": "^2.3.1",
17 | "react": "^16.8.6",
18 | "react-dom": "^16.8.6",
19 | "react-inspector": "^5.0.1"
20 | },
21 | "devDependencies": {
22 | "@types/react": "^16.8.23",
23 | "@types/react-dom": "^16.8.4",
24 | "@types/react-inspector": "^4.0.0",
25 | "@types/theme-ui": "^0.3.5",
26 | "gh-pages": "^3.1.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dxFeed/dxfeed-js-api/35a4073f6bc8e9c0add3a00f9a7d4a1f036ec754/docs/public/favicon.ico
--------------------------------------------------------------------------------
/docs/src/PromisePlayground.tsx:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2022 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import React from 'react'
9 | import { Inspector } from 'react-inspector'
10 |
11 | import Button from '@material-ui/core/Button'
12 | import FormControl from '@material-ui/core/FormControl'
13 | import FormControlLabel from '@material-ui/core/FormControlLabel'
14 | import FormLabel from '@material-ui/core/FormLabel'
15 | import Grid from '@material-ui/core/Grid'
16 | import Radio from '@material-ui/core/Radio'
17 | import RadioGroup from '@material-ui/core/RadioGroup'
18 | import Input from '@material-ui/core/Input'
19 | import { useComponents } from 'docz'
20 |
21 | import Feed, { EventType } from '../../src'
22 |
23 | const SYMBOLS = ['AAPL', 'GOOG'] as const
24 |
25 | const minusDay = (date: Date, days: number): Date => {
26 | return new Date(date.getTime() - days * 24 * 60 * 60 * 1000)
27 | }
28 |
29 | const DataViewer = ({ events }: { events: unknown[] }) =>
30 | events.length > 0 && (
31 |
32 | {events.slice(0, Math.min(10, events.length)).map((event, idx) => (
33 |
34 |
35 |
36 | ))}
37 | Only displaying 10 events, total: {events.length}
38 |
39 | )
40 |
41 | function Playground() {
42 | const { playground, pre, h2 } = useComponents()
43 | const PlaygroundComponent = playground as React.FunctionComponent<{
44 | scope: Record
45 | language: string
46 | code: string
47 | }>
48 | const PreComponent = pre as React.FunctionComponent<{}>
49 | const H2Component = h2 as React.FunctionComponent<{}>
50 |
51 | const [eventType, setEventType] = React.useState(EventType.Candle)
52 | const [symbolName, setSymbolName] = React.useState(SYMBOLS[0])
53 | const [days, setDays] = React.useState(1)
54 |
55 | const feed = React.useMemo(() => new Feed(), [])
56 |
57 | React.useEffect(() => {
58 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
59 |
60 | return () => feed.disconnect()
61 | }, [])
62 |
63 | return (
64 | <>
65 | Configure
66 |
67 |
68 |
69 |
70 | Select type
71 | {}}>
72 | } label="Time Series" />
73 |
74 |
75 |
76 |
77 |
78 |
79 | Select Event Type
80 | {
85 | setEventType(event.target.value as any)
86 | }}
87 | >
88 | {Object.keys(EventType).map((key) => {
89 | const value = key as EventType
90 |
91 | return (
92 | } label={value} />
93 | )
94 | })}
95 |
96 |
97 |
98 |
99 |
100 |
101 | Select Symbol Name
102 | {
107 | setSymbolName(event.target.value as any)
108 | }}
109 | >
110 | {SYMBOLS.map((value) => (
111 | } label={value} />
112 | ))}
113 |
114 |
115 |
116 |
117 |
118 |
119 | Period in days
120 | {
125 | const value = event.target.value
126 | setDays(value.length ? parseInt(value, 10) : 0)
127 | }}
128 | />
129 |
130 |
131 |
132 |
133 | Example
134 |
135 | Example demonstrates how to work with it in React
136 |
137 |
138 | 0
148 | this.disconnect = () => 0
149 | }
150 | },
151 | Button,
152 | DataViewer,
153 | }}
154 | language="js"
155 | code={`() => {
156 | const [events, setEvents] = React.useState([])
157 |
158 | const feed = React.useMemo(() => new Feed(), [])
159 | React.useEffect(() => {
160 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
161 | return () => feed.disconnect()
162 | }, [])
163 |
164 | const request = () => {
165 | feed.getTimeSeries(
166 | '${symbolName}${eventType === EventType.Candle ? '{=1m}' : ''}',
167 | '${eventType}',
168 | ${minusDay(new Date(), days).getTime()},
169 | ${new Date().getTime()},
170 | ).then(setEvents)
171 | }
172 |
173 | return (
174 | <>
175 |
178 |
179 | >
180 | )
181 | }`}
182 | />
183 | >
184 | )
185 | }
186 |
187 | export default Playground
188 |
--------------------------------------------------------------------------------
/docs/src/SnapshotPlayground.tsx:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2022 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import React from 'react'
9 | import { Inspector } from 'react-inspector'
10 |
11 | import Button from '@material-ui/core/Button'
12 | import FormControl from '@material-ui/core/FormControl'
13 | import FormControlLabel from '@material-ui/core/FormControlLabel'
14 | import FormLabel from '@material-ui/core/FormLabel'
15 | import Grid from '@material-ui/core/Grid'
16 | import Radio from '@material-ui/core/Radio'
17 | import RadioGroup from '@material-ui/core/RadioGroup'
18 | import Input from '@material-ui/core/Input'
19 | import { useComponents } from 'docz'
20 |
21 | import Feed, { EventType } from '../../src'
22 |
23 | const SYMBOLS = ['AAPL', 'GOOG'] as const
24 |
25 | const minusDay = (date: Date, days: number): Date => {
26 | return new Date(date.getTime() - days * 24 * 60 * 60 * 1000)
27 | }
28 |
29 | const DataViewer = ({ events }: { events: unknown[] }) =>
30 | events.length > 0 && (
31 |
32 | {events.slice(0, Math.min(10, events.length)).map((event, idx) => (
33 |
34 |
35 |
36 | ))}
37 | Only displaying 10 events, total: {events.length}
38 |
39 | )
40 |
41 | function Playground() {
42 | const { playground, pre, h2 } = useComponents()
43 | const PlaygroundComponent = playground as React.FunctionComponent<{
44 | scope: Record
45 | language: string
46 | code: string
47 | }>
48 | const PreComponent = pre as React.FunctionComponent<{}>
49 | const H2Component = h2 as React.FunctionComponent<{}>
50 |
51 | const [eventType, setEventType] = React.useState(EventType.Candle)
52 | const [symbolName, setSymbolName] = React.useState(SYMBOLS[0])
53 | const [days, setDays] = React.useState(5)
54 |
55 | const feed = React.useMemo(() => new Feed(), [])
56 |
57 | React.useEffect(() => {
58 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
59 |
60 | return () => feed.disconnect()
61 | }, [])
62 |
63 | return (
64 | <>
65 | Configure
66 |
67 |
68 |
69 |
70 | Select type
71 | {}}>
72 | } label="Time Series" />
73 |
74 |
75 |
76 |
77 |
78 |
79 | Select Event Type
80 | {
85 | setEventType(event.target.value as any)
86 | }}
87 | >
88 | {Object.keys(EventType).map((key) => {
89 | const value = key as EventType
90 |
91 | return (
92 | } label={value} />
93 | )
94 | })}
95 |
96 |
97 |
98 |
99 |
100 |
101 | Select Symbol Name
102 | {
107 | setSymbolName(event.target.value as any)
108 | }}
109 | >
110 | {SYMBOLS.map((value) => (
111 | } label={value} />
112 | ))}
113 |
114 |
115 |
116 |
117 |
118 |
119 | Period in days
120 | {
125 | const value = event.target.value
126 | setDays(value.length ? parseInt(value, 10) : 0)
127 | }}
128 | />
129 |
130 |
131 |
132 |
133 | Example
134 |
135 | Example demonstrates how to work with it in React
136 |
137 |
138 | 0
148 | this.disconnect = () => 0
149 | }
150 | },
151 | Button,
152 | DataViewer,
153 | }}
154 | language="js"
155 | code={`() => {
156 | const [events, setEvents] = React.useState([])
157 |
158 | const feed = React.useMemo(() => new Feed(), [])
159 | React.useEffect(() => {
160 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
161 | return () => feed.disconnect()
162 | }, [])
163 |
164 | const subscriptionRef = React.useRef()
165 | const subscribe = () => {
166 | // Clear
167 | subscriptionRef.current && subscriptionRef.current.unsubscribe()
168 | setEvents([])
169 |
170 | // Subscribe
171 | subscriptionRef.current = feed.subscribeTimeSeriesSnapshot(
172 | '${symbolName}${eventType === EventType.Candle ? '{=d}' : ''}',
173 | '${eventType}',
174 | ${minusDay(new Date(), days).getTime()},
175 | (snapshot) => {
176 | setEvents(snapshot)
177 | }
178 | )
179 | }
180 |
181 | return (
182 | <>
183 |
186 |
187 | >
188 | )
189 | }`}
190 | />
191 | >
192 | )
193 | }
194 |
195 | export default Playground
196 |
--------------------------------------------------------------------------------
/docs/src/SubscriptionPlayground.tsx:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import React from 'react'
9 | import { Inspector } from 'react-inspector'
10 |
11 | import Button from '@material-ui/core/Button'
12 | import FormControl from '@material-ui/core/FormControl'
13 | import FormControlLabel from '@material-ui/core/FormControlLabel'
14 | import FormLabel from '@material-ui/core/FormLabel'
15 | import Grid from '@material-ui/core/Grid'
16 | import Radio from '@material-ui/core/Radio'
17 | import RadioGroup from '@material-ui/core/RadioGroup'
18 | import { useComponents } from 'docz'
19 |
20 | import Feed, { EventType } from '../../src'
21 |
22 | const SYMBOLS = ['ETH/USD', 'EUR/USD'] as const
23 |
24 | const DataViewer = ({ play, events }: { play: boolean; events: unknown[] }) =>
25 | (play || events.length > 0) && (
26 |
27 | {events.length === 0 && Waiting events}
28 | {events.map((event, idx) => (
29 |
30 |
31 |
32 | ))}
33 |
34 | )
35 |
36 | function Playground() {
37 | const { playground, pre, h2 } = useComponents()
38 | const PlaygroundComponent = playground as React.FunctionComponent<{
39 | scope: Record
40 | language: string
41 | code: string
42 | }>
43 | const PreComponent = pre as React.FunctionComponent<{}>
44 | const H2Component = h2 as React.FunctionComponent<{}>
45 |
46 | const feedRef = React.useRef(null)
47 | const [type, setType] = React.useState<'series' | 'timeSeries'>('series')
48 | const [eventType, setEventType] = React.useState(EventType.Trade)
49 | const [symbolName, setSymbolName] = React.useState(SYMBOLS[0])
50 |
51 | const feed = React.useMemo(() => new Feed(), [])
52 |
53 | React.useEffect(() => {
54 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
55 |
56 | return () => feed.disconnect()
57 | }, [])
58 |
59 | return (
60 | <>
61 | Configure
62 |
63 |
64 |
65 |
66 | Select type
67 | {
72 | setType(event.target.value as any)
73 | }}
74 | >
75 | } label="Series" />
76 | } label="Time Series" />
77 |
78 |
79 |
80 |
81 |
82 |
83 | Select Event Type
84 | {
89 | setEventType(event.target.value as any)
90 | }}
91 | >
92 | {Object.keys(EventType).map((key) => {
93 | const value = key as EventType
94 |
95 | return (
96 | } label={value} />
97 | )
98 | })}
99 |
100 |
101 |
102 |
103 |
104 |
105 | Select Symbol Name
106 | {
111 | setSymbolName(event.target.value as any)
112 | }}
113 | >
114 | {SYMBOLS.map((value) => (
115 | } label={value} />
116 | ))}
117 |
118 |
119 |
120 |
121 |
122 | Example
123 |
124 | Example demonstrates how to work with it in React
125 |
126 |
127 | 0
137 | this.disconnect = () => 0
138 | }
139 | },
140 | Button,
141 | DataViewer,
142 | }}
143 | language="js"
144 | code={`() => {
145 | const [play, setPlay] = React.useState(false)
146 | const [forceUpdate, setForceUpdate] = React.useState(0)
147 | const [events, setEvents] = React.useState([])
148 | const handleEvent = React.useCallback((event) => {
149 | setEvents((prevState) => [...prevState, event])
150 | }, [])
151 |
152 | const feed = React.useMemo(() => new Feed(), [])
153 | React.useEffect(() => {
154 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
155 | return () => feed.disconnect()
156 | }, [])
157 |
158 | React.useEffect(() => {
159 | let unsubscribe
160 | if (play) {
161 | setEvents([])
162 | unsubscribe = feed.${
163 | type === 'series'
164 | ? `subscribe(['${eventType}'], ['${symbolName}'], handleEvent)`
165 | : `subscribeTimeSeries(['${eventType}'], ['${symbolName}'], 0, handleEvent)`
166 | }
167 | }
168 | return () => unsubscribe && unsubscribe()
169 | }, [play, forceUpdate])
170 |
171 | return (
172 | <>
173 |
176 | {play && (
177 |
180 | )}
181 |
182 | >
183 | )
184 | }`}
185 | />
186 | >
187 | )
188 | }
189 |
190 | export default Playground
191 |
--------------------------------------------------------------------------------
/docs/src/gatsby-theme-docz/wrapper.tsx:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | /* tslint:disable:file-name-casing */
9 | import * as React from 'react'
10 | import { Helmet } from 'react-helmet-async'
11 |
12 | import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'
13 | import { useColorMode } from 'theme-ui'
14 |
15 | // The doc prop contains some metadata about the page being rendered that you can use.
16 | const Wrapper: React.FunctionComponent = ({ children }) => {
17 | const [colorMode] = useColorMode()
18 | const theme = React.useMemo(
19 | () =>
20 | createMuiTheme({
21 | palette: {
22 | type: colorMode as 'light' | 'dark',
23 | },
24 | }),
25 | [colorMode]
26 | )
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | export default Wrapper
40 |
--------------------------------------------------------------------------------
/docs/src/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Getting Started
3 | route: /
4 | ---
5 |
6 | # Getting Started
7 |
8 | This package provides access to [dxFeed](https://www.dxfeed.com/) streaming data.
9 |
10 | Our package is easy to integrate with any modern framework.
11 |
12 | On this website, you will find usage examples.
13 |
14 | ## Install
15 | Install package from NPM
16 |
17 | ```sh
18 | npm install @dxfeed/api
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/src/promise-playground.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Promise
3 | route: /playground/promise
4 | menu: Playground
5 | ---
6 |
7 | # Promise
8 |
9 | import Playground from './PromisePlayground'
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/src/snapshot-playground.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Snapshot
3 | route: /playground/snapshot
4 | menu: Playground
5 | ---
6 |
7 | # Snapshot
8 |
9 | import Playground from './SnapshotPlayground'
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/src/subscription-playground.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Subscription
3 | route: /playground/subscription
4 | menu: Playground
5 | ---
6 |
7 | # Subscription Playground
8 |
9 | import Playground from './SubscriptionPlayground'
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/src/usage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | name: Basic Usage
3 | route: /basic-usage
4 | ---
5 |
6 | # NodeJS usage
7 |
8 | Install `cometd-nodejs-client` package
9 |
10 | ```sh
11 | npm install cometd-nodejs-client
12 | ```
13 |
14 | and use it in your code
15 |
16 | ```js
17 | require('cometd-nodejs-client').adapt()
18 | // or
19 | import * as CometdNodejsClient from 'cometd-nodejs-client'
20 | CometdNodejsClient.adapt()
21 | ```
22 |
23 | # Basic Usage
24 |
25 | We have several classes in implementation:
26 |
27 | - Feed **_(public)_**
28 | - Endpoint **_(private)_**
29 | - Subscriptions **_(private)_**
30 |
31 | The _Feed_ is entry point for configuration and creating subscriptions.
32 | _Feed_ manages private classes for connecting and subscribing.
33 | The _Endpoint_ is responsible for managing the web socket connection.
34 | _Subscriptions_ for managing open subscriptions.
35 |
36 | ## Import package
37 |
38 | ```ts
39 | import Feed from '@dxfeed/api'
40 | ```
41 |
42 | ## Configure & Create connection
43 |
44 | Create instance of Feed.
45 |
46 | ```ts
47 | const feed = new Feed()
48 | ```
49 |
50 | Provide auth token if needed.
51 |
52 | ```ts
53 | feed.setAuthToken('authToken')
54 | ```
55 |
56 | Set web socket address and open connection.
57 |
58 | ```ts
59 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
60 | ```
61 |
62 | ## Configure & Create subscription
63 |
64 | You should specify event types and symbol names.
65 |
66 | ```ts
67 | feed.subscribe<{ value: number }>(
68 | [EventType.Summary, EventType.Trade] /* event types */,
69 | ['AEX.IND:TEI'] /* symbols */,
70 | handleEvent
71 | )
72 | ```
73 |
74 | For timed subscription you should also provide time to start subscription from.
75 |
76 | For Candle event type along with base symbol, you should specify an aggregation period. You can also set price type. More details: https://kb.dxfeed.com/display/DS/REST+API#RESTAPI-Candlesymbols.
77 |
78 | ```ts
79 | feed.subscribeTimeSeries<{ value: number }>(
80 | [EventType.Summary, EventType.Trade] /* event types */,
81 | ['AEX.IND:TEI'] /* symbols */,
82 | 0 /* fromTime */,
83 | handleEvent
84 | )
85 | ```
86 |
87 | Last argument its event handler for process incoming events.
88 |
89 | ```ts
90 | const handleEvent = (event) => {
91 | /* process event */
92 | }
93 | ```
94 |
95 | ## Close subscription
96 |
97 | All subscribe methods return unsubscribe handler, you need to call this method for unsubscribe.
98 |
99 | ```ts
100 | const unsubscribe = feed.subscribe(eventTypes, symbols, handleEvent)
101 |
102 | onExit(() => unsubscribe())
103 | ```
104 |
105 | ## Aggregated API
106 |
107 | ### Get TimeSeries
108 |
109 | If you want to get TimeSeries events for a given time period, refer to example below.
110 |
111 | ```ts
112 | // inside async function
113 | const events = await feed.getTimeSeries(
114 | 'AAPL{=15m}',
115 | EventType.Candle,
116 | fromDate.getTime(),
117 | toDate.getTime()
118 | )
119 | ```
120 |
121 | ### Subscribe TimeSeries snapshot
122 |
123 | If you want to subscribe to TimeSeries events, refer to example below.
124 |
125 | ```ts
126 | const unsubscribe = feed.subscribeTimeSeriesSnapshot('AAPL{=15m}', EventType.Candle, (candles) => {
127 | // process candles
128 | chart.setCandles(candles)
129 | })
130 | ```
131 |
132 | ## Close connection
133 |
134 | If you need to close the web socket connection
135 |
136 | ```ts
137 | feed.disconnect()
138 | ```
139 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "moduleResolution": "node",
5 | "strict": true,
6 | "resolveJsonModule": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": false,
9 | "noEmit": true,
10 | "jsx": "react"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/nodejs-candles.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * This example demonstrates how to subscribe to a snapshot of time series in Node.js.
3 | */
4 | const { Feed, EventType } = require('@dxfeed/api')
5 |
6 | // We are using `cometd` library to connect to the feed, so we need to adapt it to work in Node.js.
7 | require('cometd-nodejs-client').adapt()
8 |
9 | const feed = new Feed()
10 | // Connect to the demo dxFeed feed
11 | // Demo feed is a limited version of dxFeed Feed API, which provides delayed market data
12 | feed.connect('wss://demo.dxfeed.com/webservice/cometd')
13 |
14 | // Subscribe to a snapshot Candles
15 | feed.subscribeTimeSeriesSnapshot(
16 | 'EUR/USD:AFX{=d,mm=CFH2,price=mark}', // subscription symbol
17 | EventType.Candle, // subscription event type
18 | new Date('2023-07-25T14:00:00').getTime(), // subscription fromTime
19 | (snapshot) => {
20 | console.log('Candles', snapshot)
21 | // process snapshot, for example chart.setCandles(snapshot)
22 | }
23 | )
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@dxfeed/api",
3 | "version": "1.6.0",
4 | "description": "This package provides access to dxFeed streaming data",
5 | "type": "module",
6 | "main": "lib/index.cjs",
7 | "module": "lib/index.mjs",
8 | "types": "lib/index.d.ts",
9 | "repository": "https://github.com/dxFeed/dxfeed-js-api.git",
10 | "author": "Dmitry Petrov ",
11 | "license": "MPL-2.0",
12 | "scripts": {
13 | "prebuild": "rimraf lib",
14 | "build": "rollup -c",
15 | "docs": "typedoc src/index.ts --out examples/public/docs",
16 | "format": "prettier --write \"src/**/*.ts\"",
17 | "lint": "tslint -p tsconfig.json",
18 | "test": "jest src"
19 | },
20 | "dependencies": {
21 | "@types/cometd": "^4.0.7",
22 | "cometd": "^5.0.1"
23 | },
24 | "devDependencies": {
25 | "@types/jest": "^26.0.5",
26 | "esbuild": "^0.18.4",
27 | "jest": "^26.1.0",
28 | "prettier": "^2.0.5",
29 | "rimraf": "^3.0.2",
30 | "rollup": "^3.25.1",
31 | "rollup-plugin-dts": "^5.3.0",
32 | "rollup-plugin-esbuild": "^5.0.0",
33 | "ts-jest": "^26.1.3",
34 | "tslint": "^6.1.1",
35 | "tslint-config-prettier": "^1.18.0",
36 | "typescript": "^4.9.5"
37 | },
38 | "jest": {
39 | "preset": "ts-jest"
40 | },
41 | "prettier": {
42 | "trailingComma": "es5",
43 | "tabWidth": 2,
44 | "semi": false,
45 | "singleQuote": true,
46 | "printWidth": 100
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import dts from 'rollup-plugin-dts'
2 | import esbuild from 'rollup-plugin-esbuild'
3 |
4 | const name = 'lib/index'
5 |
6 | const bundle = config => ({
7 | ...config,
8 | input: 'src/index.ts',
9 | external: id => !/^[./]/.test(id),
10 | })
11 |
12 | export default [
13 | bundle({
14 | plugins: [esbuild()],
15 | output: [
16 | {
17 | file: `${name}.cjs`,
18 | format: 'cjs',
19 | sourcemap: true,
20 | },
21 | {
22 | file: `${name}.mjs`,
23 | format: 'es',
24 | sourcemap: true,
25 | },
26 | ],
27 | }),
28 | bundle({
29 | plugins: [dts()],
30 | output: {
31 | file: `${name}.d.ts`,
32 | format: 'es',
33 | },
34 | }),
35 | ]
--------------------------------------------------------------------------------
/src/__mocks__/endpoint.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | /* tslint:disable:no-empty */
9 |
10 | type StateChangeHandler = (state: any) => void
11 | type DataChangeHandler = (message: any, state: boolean) => void
12 |
13 | export class Endpoint {
14 | handlers: {
15 | onStateChange?: StateChangeHandler
16 | onData?: DataChangeHandler
17 | } = {}
18 | mock = true
19 |
20 | constructor() {}
21 |
22 | registerStateChangeHandler = jest.fn((onStateChange: StateChangeHandler) => {
23 | this.handlers.onStateChange = onStateChange
24 | })
25 |
26 | registerDataChangeHandler = jest.fn((onData: DataChangeHandler) => {
27 | this.handlers.onData = onData
28 | })
29 |
30 | connected = jest.fn(() => true)
31 |
32 | setAuthToken = jest.fn() // (_token: string) {}
33 |
34 | disconnect = jest.fn()
35 |
36 | updateSubscriptions = jest.fn() // (message: ISubscribeMessage) {}
37 | }
38 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | export const HEADER_AUTH_TOKEN_KEY = 'com.devexperts.auth.AuthToken'
9 |
--------------------------------------------------------------------------------
/src/endpoint.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import { CometD, Configuration, Message } from 'cometd'
9 |
10 | import { HEADER_AUTH_TOKEN_KEY } from './config'
11 | import { IFeedImplState, IncomingData, ISubscribeMessage } from './interfaces'
12 |
13 | type StateChangeHandler = (state: Partial) => void
14 |
15 | type DataChangeHandler = (message: IncomingData, timeSeries: boolean) => void
16 |
17 | export class Endpoint {
18 | cometd: CometD | null = null
19 | authToken: string | null = null
20 | connectionAlive: boolean = false
21 |
22 | handlers: {
23 | onStateChange?: StateChangeHandler
24 | onData?: DataChangeHandler
25 | } = {}
26 |
27 | registerStateChangeHandler = (onStateChange: StateChangeHandler) => {
28 | this.handlers.onStateChange = onStateChange
29 | }
30 |
31 | registerDataChangeHandler = (onData: DataChangeHandler) => {
32 | this.handlers.onData = onData
33 | }
34 |
35 | private updateConnectedState(connected: boolean) {
36 | if (this.connectionAlive !== connected) {
37 | this.connectionAlive = connected
38 | this.handlers?.onStateChange({ connected })
39 | }
40 | }
41 |
42 | private onMetaUpdate = (message: Message) => {
43 | if (!this.isConnected()) {
44 | return this.updateConnectedState(false)
45 | }
46 |
47 | this.updateConnectedState(message.successful)
48 | }
49 |
50 | private onMetaUnsuccessful = () => {
51 | this.updateConnectedState(false)
52 | }
53 |
54 | private onServiceState = (message: Message) => {
55 | this.handlers?.onStateChange(message.data)
56 | }
57 |
58 | private onServiceData = (message: Message) => {
59 | this.handlers?.onData(message.data, false)
60 | }
61 |
62 | private onServiceTimeSeriesData = (message: Message) => {
63 | this.handlers?.onData(message.data, true)
64 | }
65 |
66 | isConnected = () => this.cometd !== null && !this.cometd.isDisconnected()
67 |
68 | connect(config: Configuration) {
69 | const cometd = this.cometd || this.createCometD()
70 |
71 | cometd.configure(config)
72 |
73 | const authToken = this.authToken
74 | if (authToken === null) {
75 | // @ts-ignore cometd types differs from code, need research
76 | cometd.handshake()
77 | } else {
78 | // @ts-ignore cometd types differs from code, need research
79 | cometd.handshake({ ext: { [HEADER_AUTH_TOKEN_KEY]: authToken } })
80 | }
81 | }
82 |
83 | private createCometD = () => {
84 | const cometd = new CometD()
85 |
86 | cometd.addListener('/meta/handshake', this.onMetaUpdate)
87 | cometd.addListener('/meta/connect', this.onMetaUpdate)
88 | cometd.addListener('/meta/unsuccessful', this.onMetaUnsuccessful)
89 | cometd.addListener('/service/state', this.onServiceState)
90 | cometd.addListener('/service/data', this.onServiceData)
91 | cometd.addListener('/service/timeSeriesData', this.onServiceTimeSeriesData)
92 |
93 | this.cometd = cometd
94 | return cometd
95 | }
96 |
97 | setAuthToken(token: string) {
98 | this.authToken = token
99 | }
100 |
101 | disconnect() {
102 | if (this.cometd !== null) {
103 | // @ts-ignore cometd types differs from code, need research
104 | this.cometd.disconnect(true)
105 | this.cometd = null
106 | }
107 | }
108 |
109 | updateSubscriptions(message: ISubscribeMessage) {
110 | return this.publish('sub', message)
111 | }
112 |
113 | private publish(service: 'sub', message: object) {
114 | if (!this.cometd) {
115 | throw new Error('CometD not connected')
116 | }
117 |
118 | return this.cometd.publish('/service/' + service, message)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/eventFlags.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | export enum EventFlag {
9 | /**
10 | * (0x01) TX_PENDING indicates a pending transactional update. When TX_PENDING is 1, it means that an ongoing transaction
11 | * update, that spans multiple events, is in process.
12 | */
13 | TxPending = 0x01,
14 |
15 | /**
16 | * (0x02) REMOVE_EVENT indicates that the event with the corresponding index has to be removed.
17 | */
18 | RemoveEvent = 0x02,
19 |
20 | /**
21 | * (0x04) SNAPSHOT_BEGIN indicates when the loading of a snapshot starts. Snapshot load starts on new subscription and
22 | * the first indexed event that arrives for each exchange code (in the case of a regional record) on a new
23 | * subscription may have SNAPSHOT_BEGIN set to true. It means that an ongoing snapshot consisting of multiple
24 | * events is incoming.
25 | */
26 | SnapshotBegin = 0x04,
27 |
28 | /**
29 | * (0x08) SNAPSHOT_END or (0x10) SNAPSHOT_SNIP indicates the end of a snapshot. The difference between SNAPSHOT_END and
30 | * SNAPSHOT_SNIP is the following: SNAPSHOT_END indicates that the data source sent all the data pertaining to
31 | * the subscription for the corresponding indexed event, while SNAPSHOT_SNIP indicates that some limit on the
32 | * amount of data was reached and while there still might be more data available, it will not be provided.
33 | */
34 | SnapshotEnd = 0x08,
35 |
36 | /**
37 | * (0x08) SNAPSHOT_END or (0x10) SNAPSHOT_SNIP indicates the end of a snapshot. The difference between SNAPSHOT_END and
38 | * SNAPSHOT_SNIP is the following: SNAPSHOT_END indicates that the data source sent all the data pertaining to
39 | * the subscription for the corresponding indexed event, while SNAPSHOT_SNIP indicates that some limit on the
40 | * amount of data was reached and while there still might be more data available, it will not be provided.
41 | */
42 | SnapshotSnip = 0x10,
43 | }
44 |
45 | export interface EventFlags {
46 | txPending: boolean
47 | shouldBeRemoved: boolean
48 | snapshotBegin: boolean
49 | snapshotEnd: boolean
50 | snapshotSnip: boolean
51 | }
52 |
53 | /* tslint:disable:no-bitwise */
54 | export const parseEventFlags = (flags: number): EventFlags => ({
55 | txPending: (flags & EventFlag.TxPending) > 0,
56 | shouldBeRemoved: (flags & EventFlag.RemoveEvent) > 0,
57 | snapshotBegin: (flags & EventFlag.SnapshotBegin) > 0,
58 | snapshotEnd: (flags & EventFlag.SnapshotEnd) > 0,
59 | snapshotSnip: (flags & EventFlag.SnapshotSnip) > 0,
60 | })
61 |
--------------------------------------------------------------------------------
/src/feed.test.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | jest.mock('./endpoint')
9 |
10 | import { EventFlag } from './eventFlags'
11 | import { Feed, AbortedError, TimeoutError } from './feed'
12 | import { EventType, IncomingData } from './interfaces'
13 |
14 | describe('Feed', () => {
15 | let instance: Feed
16 |
17 | beforeEach(() => {
18 | jest.useFakeTimers()
19 | instance = new Feed()
20 | })
21 |
22 | it('mock should work', () => {
23 | expect((instance.endpoint as any).mock).toBeTruthy()
24 | })
25 |
26 | it('should trigger series sub event on data', () => {
27 | const eventType = EventType.Summary
28 | const eventSymbol = 'AEX.IND:TEI'
29 |
30 | let dataResults
31 | const handleEvent = jest.fn((data) => {
32 | dataResults = data
33 | })
34 |
35 | instance.subscribe([eventType], [eventSymbol], handleEvent)
36 |
37 | instance.endpoint.handlers?.onData?.(
38 | [
39 | [eventType, ['eventSymbol', 'eventTime']],
40 | [eventSymbol, 0],
41 | ],
42 | false
43 | )
44 |
45 | jest.runAllTimers()
46 |
47 | expect(handleEvent).toBeCalled()
48 |
49 | expect(dataResults).toEqual({
50 | eventSymbol,
51 | eventTime: 0,
52 | eventType,
53 | })
54 | })
55 |
56 | it('should trigger time series sub event on data', () => {
57 | const eventType = EventType.Candle
58 | const eventSymbol = 'AEX.IND:TEI{=15m}'
59 |
60 | let dataResults
61 | const handleEvent = jest.fn((data) => {
62 | dataResults = data
63 | })
64 |
65 | instance.subscribeTimeSeries([eventType], [eventSymbol], 0, handleEvent)
66 |
67 | instance.endpoint.handlers?.onData?.(
68 | [
69 | [eventType, ['eventSymbol', 'eventTime', 'eventFlags', 'index', 'time']],
70 | [
71 | ...[eventSymbol, 0, 0, 6829250544717005000, 1590058800000],
72 | ...[eventSymbol, 0, 0, 6829246679246438000, 1590057900000],
73 | ],
74 | ],
75 | true
76 | )
77 |
78 | jest.runAllTimers()
79 |
80 | expect(handleEvent).toBeCalled()
81 | expect(dataResults).toBeDefined()
82 |
83 | expect(dataResults).toHaveProperty('eventType', eventType)
84 | expect(dataResults).toHaveProperty('eventSymbol', eventSymbol)
85 | })
86 |
87 | // TODO: Нужен тест проверяющий разные типы обработки Data, когда уже есть структура в памяти для какого-то эвента
88 | })
89 |
90 | describe('Feed - subscriptions', () => {
91 | let instance: Feed
92 |
93 | const eventType = EventType.Summary
94 | const symbolsSet1 = ['1', '2', '3', '4']
95 | const symbolsSet2 = ['2', '3']
96 |
97 | const createSubscription = (symbols: string[]) =>
98 | instance.subscribe([eventType], symbols, () => 0)
99 |
100 | const createMultipleSubscriptions = (symbols: string[]) =>
101 | symbols.map((symbol) => instance.subscribe([eventType], [symbol], () => 0))
102 |
103 | beforeEach(() => {
104 | jest.useFakeTimers()
105 | instance = new Feed()
106 | })
107 |
108 | it('should unsubscribe crossing subscriptions correctly', () => {
109 | const publishFirstTime = jest.fn()
110 | instance.endpoint.updateSubscriptions = publishFirstTime
111 |
112 | const unsubscribe1 = createSubscription(symbolsSet1)
113 |
114 | const unsubscribe2 = createSubscription(symbolsSet2)
115 |
116 | jest.runAllTimers()
117 |
118 | const publishSecondTime = jest.fn()
119 | instance.endpoint.updateSubscriptions = publishSecondTime
120 |
121 | unsubscribe2()
122 |
123 | jest.runAllTimers()
124 |
125 | const publishThirdTime = jest.fn()
126 | instance.endpoint.updateSubscriptions = publishThirdTime
127 |
128 | unsubscribe1()
129 |
130 | jest.runAllTimers()
131 |
132 | expect(publishFirstTime).toBeCalledTimes(1)
133 | expect(publishFirstTime).toBeCalledWith({ add: { [eventType]: symbolsSet1 } })
134 |
135 | expect(publishSecondTime).toBeCalledTimes(0)
136 |
137 | expect(publishThirdTime).toBeCalledTimes(1)
138 | expect(publishThirdTime).toBeCalledWith({
139 | remove: { [eventType]: symbolsSet1 },
140 | })
141 | })
142 |
143 | it('should remove and add correctly in 1 tick', () => {
144 | const publishFirstTime = jest.fn()
145 | instance.endpoint.updateSubscriptions = publishFirstTime
146 |
147 | const unsubscribeSet1 = createMultipleSubscriptions(symbolsSet1)
148 |
149 | jest.runAllTimers()
150 |
151 | const publishSecondTime = jest.fn()
152 | instance.endpoint.updateSubscriptions = publishSecondTime
153 |
154 | unsubscribeSet1.forEach((unsubscribe) => {
155 | unsubscribe()
156 | })
157 |
158 | createMultipleSubscriptions(symbolsSet2)
159 |
160 | jest.runAllTimers()
161 |
162 | expect(publishFirstTime).toBeCalledTimes(1)
163 | expect(publishFirstTime).toBeCalledWith({ add: { [eventType]: symbolsSet1 } })
164 |
165 | expect(publishSecondTime).toBeCalledTimes(1)
166 | expect(publishSecondTime).toBeCalledWith({
167 | add: { [eventType]: symbolsSet2 },
168 | remove: { [eventType]: symbolsSet1 },
169 | })
170 | })
171 |
172 | it('should remove and add correctly in different ticks', () => {
173 | const publishFirstTime = jest.fn()
174 | instance.endpoint.updateSubscriptions = publishFirstTime
175 |
176 | const unsubscribeSet1 = createMultipleSubscriptions(symbolsSet1)
177 |
178 | jest.runAllTimers()
179 |
180 | const publishSecondTime = jest.fn()
181 | instance.endpoint.updateSubscriptions = publishSecondTime
182 |
183 | unsubscribeSet1.forEach((unsubscribe) => {
184 | unsubscribe()
185 | })
186 |
187 | jest.runAllTimers()
188 |
189 | const publishThirdTime = jest.fn()
190 | instance.endpoint.updateSubscriptions = publishThirdTime
191 |
192 | createMultipleSubscriptions(symbolsSet2)
193 |
194 | jest.runAllTimers()
195 |
196 | expect(publishFirstTime).toBeCalledTimes(1)
197 | expect(publishFirstTime).toBeCalledWith({ add: { [eventType]: symbolsSet1 } })
198 |
199 | expect(publishSecondTime).toBeCalledTimes(1)
200 | expect(publishSecondTime).toBeCalledWith({
201 | remove: { [eventType]: symbolsSet1 },
202 | })
203 |
204 | expect(publishThirdTime).toBeCalledTimes(1)
205 | expect(publishThirdTime).toBeCalledWith({
206 | add: { [eventType]: symbolsSet2 },
207 | })
208 | })
209 |
210 | it('should send subscription immediately if queue is full', () => {
211 | const publishFirstTime = jest.fn()
212 | instance.endpoint.updateSubscriptions = publishFirstTime
213 |
214 | const longSymbolSet = new Array(400).fill(0).map((_, idx) => idx.toString())
215 | const unsubscribe = createSubscription(longSymbolSet)
216 |
217 | jest.runAllTimers()
218 |
219 | const publishSecondTime = jest.fn()
220 | instance.endpoint.updateSubscriptions = publishSecondTime
221 |
222 | unsubscribe()
223 |
224 | jest.runAllTimers()
225 |
226 | expect(publishFirstTime).toBeCalledTimes(2)
227 |
228 | expect(publishSecondTime).toBeCalledTimes(2)
229 | })
230 | })
231 |
232 | describe('Feed - subscriptions time series', () => {
233 | let instance: Feed
234 |
235 | const eventType = EventType.Candle
236 | const symbolsSet1 = ['1', '2', '3', '4']
237 | const symbolsSet2 = ['2', '3']
238 |
239 | const createSubscriptionTimeSeries = (symbols: string[], fromTime: number) =>
240 | instance.subscribeTimeSeries([eventType], symbols, fromTime, () => 0)
241 |
242 | beforeEach(() => {
243 | jest.useFakeTimers()
244 | instance = new Feed()
245 | })
246 |
247 | it('should unsubscribe crossing subscriptions correctly', () => {
248 | const publishFirstTime = jest.fn()
249 | instance.endpoint.updateSubscriptions = publishFirstTime
250 |
251 | const fromTime1 = 1000
252 | const fromTime2 = 10
253 |
254 | const unsubscribe1 = createSubscriptionTimeSeries(symbolsSet1, fromTime1)
255 |
256 | const unsubscribe2 = createSubscriptionTimeSeries(symbolsSet2, fromTime2)
257 |
258 | jest.runAllTimers()
259 |
260 | const publishSecondTime = jest.fn()
261 | instance.endpoint.updateSubscriptions = publishSecondTime
262 |
263 | unsubscribe2()
264 |
265 | jest.runAllTimers()
266 |
267 | const publishThirdTime = jest.fn()
268 | instance.endpoint.updateSubscriptions = publishThirdTime
269 |
270 | unsubscribe1()
271 |
272 | jest.runAllTimers()
273 |
274 | expect(publishFirstTime).toBeCalledTimes(1)
275 | expect(publishFirstTime).toBeCalledWith({
276 | addTimeSeries: {
277 | [eventType]: [
278 | { eventSymbol: '1', fromTime: fromTime1 },
279 | { eventSymbol: '2', fromTime: fromTime2 },
280 | { eventSymbol: '3', fromTime: fromTime2 },
281 | { eventSymbol: '4', fromTime: fromTime1 },
282 | ],
283 | },
284 | })
285 |
286 | expect(publishSecondTime).toBeCalledTimes(1)
287 | expect(publishSecondTime).toBeCalledWith({
288 | addTimeSeries: {
289 | [eventType]: [
290 | { eventSymbol: '2', fromTime: fromTime1 },
291 | { eventSymbol: '3', fromTime: fromTime1 },
292 | ],
293 | },
294 | })
295 |
296 | expect(publishThirdTime).toBeCalledTimes(1)
297 | expect(publishThirdTime).toBeCalledWith({
298 | removeTimeSeries: { [eventType]: symbolsSet1 },
299 | })
300 | })
301 |
302 | it('should send subscription immediately if queue is full', () => {
303 | const publishFirstTime = jest.fn()
304 | instance.endpoint.updateSubscriptions = publishFirstTime
305 |
306 | const longSymbolSet = new Array(400).fill(0).map((_, idx) => idx.toString())
307 | const unsubscribe = createSubscriptionTimeSeries(longSymbolSet, 0)
308 |
309 | jest.runAllTimers()
310 |
311 | const publishSecondTime = jest.fn()
312 | instance.endpoint.updateSubscriptions = publishSecondTime
313 |
314 | unsubscribe()
315 |
316 | jest.runAllTimers()
317 |
318 | expect(publishFirstTime).toBeCalledTimes(2)
319 |
320 | expect(publishSecondTime).toBeCalledTimes(2)
321 | })
322 | })
323 |
324 | describe('Feed - promises time series', () => {
325 | let instance: Feed
326 |
327 | beforeEach(() => {
328 | jest.useFakeTimers()
329 | instance = new Feed()
330 | })
331 |
332 | const newMockDataHead = (eventType: EventType): [EventType, string[]] => [
333 | eventType,
334 | ['eventSymbol', 'eventTime', 'eventFlags', 'index', 'time'],
335 | ]
336 | const newMockDataBody = (
337 | eventSymbol: string,
338 | time: number,
339 | eventFlags: number,
340 | index: number
341 | ): (string | number)[] => [eventSymbol, 0, eventFlags, index, time]
342 |
343 | describe('aggregation logic', () => {
344 | const ONE_DAY = 1000 * 60 * 60 * 24
345 | const TO_TIME = new Date().getTime()
346 | const FROM_TIME = TO_TIME - 7 * ONE_DAY
347 |
348 | const SYMBOL = 'AAPL'
349 | const EVENT_TYPE = EventType.Candle
350 |
351 | const pushData = (data: IncomingData) => {
352 | instance.endpoint.handlers?.onData?.(data, true)
353 | }
354 |
355 | test('should finish aggergation on snip flag', async () => {
356 | const promise = instance.getTimeSeries(SYMBOL, EVENT_TYPE, FROM_TIME, TO_TIME)
357 |
358 | pushData([
359 | newMockDataHead(EVENT_TYPE),
360 | [
361 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 0),
362 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY * 2, EventFlag.SnapshotSnip, 1),
363 | ],
364 | ])
365 |
366 | const events = await promise
367 | expect(events).toStrictEqual([
368 | expect.objectContaining({ time: TO_TIME - ONE_DAY, index: 0 }),
369 | expect.objectContaining({ time: TO_TIME - ONE_DAY * 2, index: 1 }),
370 | ])
371 | })
372 |
373 | test('should finish aggergation after getting message with time === from', async () => {
374 | const promise = instance.getTimeSeries(SYMBOL, EVENT_TYPE, FROM_TIME, TO_TIME)
375 |
376 | pushData([
377 | newMockDataHead(EVENT_TYPE),
378 | [
379 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 0),
380 | ...newMockDataBody(SYMBOL, FROM_TIME, 0, 1),
381 | ],
382 | ])
383 |
384 | const events = await promise
385 | expect(events).toStrictEqual([
386 | expect.objectContaining({ time: TO_TIME - ONE_DAY, index: 0 }),
387 | expect.objectContaining({ time: FROM_TIME, index: 1 }),
388 | ])
389 | })
390 |
391 | test('should return data sorted by index', async () => {
392 | const promise = instance.getTimeSeries(SYMBOL, EVENT_TYPE, FROM_TIME, TO_TIME)
393 |
394 | pushData([
395 | newMockDataHead(EVENT_TYPE),
396 | [
397 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 2),
398 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 1),
399 | ...newMockDataBody(SYMBOL, TO_TIME - 1, 0, 50),
400 | ...newMockDataBody(SYMBOL, TO_TIME - 2, 0, 10),
401 | ...newMockDataBody(SYMBOL, TO_TIME - 3, EventFlag.SnapshotSnip, 100),
402 | ],
403 | ])
404 |
405 | const events = await promise
406 | expect(events).toStrictEqual([
407 | expect.objectContaining({ time: TO_TIME - ONE_DAY, index: 1 }),
408 | expect.objectContaining({ time: TO_TIME - ONE_DAY, index: 2 }),
409 | expect.objectContaining({ time: TO_TIME - 2, index: 10 }),
410 | expect.objectContaining({ time: TO_TIME - 1, index: 50 }),
411 | expect.objectContaining({ time: TO_TIME - 3, index: 100 }),
412 | ])
413 | })
414 |
415 | test('aggregation should work until pending flag is false', async () => {
416 | const promise = instance.getTimeSeries(SYMBOL, EVENT_TYPE, FROM_TIME, TO_TIME)
417 |
418 | pushData([
419 | newMockDataHead(EVENT_TYPE),
420 | [
421 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 0),
422 | ...newMockDataBody(SYMBOL, FROM_TIME, EventFlag.TxPending, 1),
423 | ...newMockDataBody(SYMBOL, TO_TIME - 1, EventFlag.TxPending, 2),
424 | ...newMockDataBody(SYMBOL, FROM_TIME, 0, 3),
425 | ],
426 | ])
427 |
428 | const events = await promise
429 | expect(events).toStrictEqual([
430 | expect.objectContaining({ time: TO_TIME - ONE_DAY, index: 0 }),
431 | expect.objectContaining({ time: FROM_TIME, index: 1 }),
432 | expect.objectContaining({ time: TO_TIME - 1, index: 2 }),
433 | expect.objectContaining({ time: FROM_TIME, index: 3 }),
434 | ])
435 | })
436 |
437 | test('events with delete flag should be removed from event list', async () => {
438 | const promise = instance.getTimeSeries(SYMBOL, EVENT_TYPE, FROM_TIME, TO_TIME)
439 |
440 | pushData([
441 | newMockDataHead(EVENT_TYPE),
442 | [
443 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 0),
444 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, EventFlag.RemoveEvent, 0),
445 | ...newMockDataBody(SYMBOL, TO_TIME - 2 * ONE_DAY, EventFlag.SnapshotSnip, 1),
446 | ],
447 | ])
448 |
449 | const events = await promise
450 | expect(events).toStrictEqual([
451 | expect.objectContaining({ time: TO_TIME - 2 * ONE_DAY, index: 1 }),
452 | ])
453 | })
454 |
455 | test('messages out of time range should be ignored', async () => {
456 | const promise = instance.getTimeSeries(SYMBOL, EVENT_TYPE, FROM_TIME, TO_TIME)
457 |
458 | pushData([
459 | newMockDataHead(EVENT_TYPE),
460 | [
461 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY, 0, 0),
462 | ...newMockDataBody(SYMBOL, FROM_TIME - 1, 0, 1),
463 | ...newMockDataBody(SYMBOL, FROM_TIME - 2, 0, 2),
464 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY - 1, 0, 3),
465 | ...newMockDataBody(SYMBOL, TO_TIME - ONE_DAY - 2, EventFlag.SnapshotSnip, 4),
466 | ],
467 | ])
468 |
469 | const events = await promise
470 | expect(events).toStrictEqual([
471 | expect.objectContaining({ time: TO_TIME - ONE_DAY, index: 0 }),
472 | expect.objectContaining({ time: TO_TIME - ONE_DAY - 1, index: 3 }),
473 | expect.objectContaining({ time: TO_TIME - ONE_DAY - 2, index: 4 }),
474 | ])
475 | })
476 | })
477 |
478 | describe('cleanup', () => {
479 | const setupExpectCleanup = () => {
480 | instance.endpoint.updateSubscriptions = jest.fn()
481 |
482 | return () => {
483 | expect(instance.endpoint.updateSubscriptions).toHaveBeenCalledWith(
484 | expect.objectContaining({
485 | removeTimeSeries: {
486 | [EventType.Candle]: ['AAPL'],
487 | },
488 | })
489 | )
490 | }
491 | }
492 |
493 | test('cleanup on abort should work', () => {
494 | const expectCleanup = setupExpectCleanup()
495 |
496 | const abortController = new AbortController()
497 | expect(
498 | instance.getTimeSeries(
499 | 'AAPL',
500 | EventType.Candle,
501 | new Date().getTime(),
502 | new Date().getTime(),
503 | {
504 | signal: abortController.signal,
505 | }
506 | )
507 | ).rejects.toThrowError(AbortedError)
508 |
509 | abortController.abort()
510 | // let time to send unsub message
511 | jest.advanceTimersByTime(100)
512 |
513 | expectCleanup()
514 | })
515 |
516 | test('cleanup on timeout should work', () => {
517 | const expectCleanup = setupExpectCleanup()
518 |
519 | expect(
520 | instance.getTimeSeries('AAPL', EventType.Candle, new Date().getTime(), new Date().getTime())
521 | ).rejects.toThrowError(TimeoutError)
522 |
523 | jest.advanceTimersByTime(16_000)
524 |
525 | expectCleanup()
526 | })
527 |
528 | test('cleanup on aggergation finish should work', () => {
529 | const expectCleanup = setupExpectCleanup()
530 |
531 | expect(
532 | instance.getTimeSeries('AAPL', EventType.Candle, new Date().getTime(), new Date().getTime())
533 | ).resolves.not.toThrow()
534 | instance.endpoint.handlers?.onData?.(
535 | [
536 | newMockDataHead(EventType.Candle),
537 | newMockDataBody('AAPL', new Date().getTime(), EventFlag.SnapshotSnip, 0),
538 | ],
539 | true
540 | )
541 |
542 | // let aggregation finish
543 | jest.advanceTimersByTime(1000)
544 |
545 | expectCleanup()
546 | })
547 | })
548 | })
549 |
--------------------------------------------------------------------------------
/src/feed.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import { Endpoint } from './endpoint'
9 | import { parseEventFlags } from './eventFlags'
10 | import { EventType, IEvent, ITimeSeriesEvent } from './interfaces'
11 | import { Subscriptions } from './subscriptions'
12 | import {
13 | isFinishedTimeSeriesAggregationResult,
14 | newTimeSeriesAggregator,
15 | } from './timeSeriesAggregator'
16 | import { newPromiseWithResource } from './utils'
17 |
18 | /* tslint:disable:max-classes-per-file */
19 | export class TimeoutError extends Error {}
20 | export class AbortedError extends Error {}
21 |
22 | export class Feed {
23 | endpoint: Endpoint
24 | subscriptions: Subscriptions
25 |
26 | constructor() {
27 | this.endpoint = new Endpoint()
28 | this.subscriptions = new Subscriptions(this.endpoint)
29 | }
30 |
31 | setAuthToken = (token: string) => {
32 | this.endpoint.setAuthToken(token)
33 | }
34 |
35 | connect = (url: string) => {
36 | this.endpoint.connect({ url })
37 | }
38 |
39 | disconnect = () => {
40 | this.endpoint.disconnect()
41 | }
42 |
43 | subscribe = (
44 | eventTypes: EventType[],
45 | eventSymbols: string[],
46 | onChange: (event: TEvent) => void
47 | ) => this.subscriptions.subscribe(eventTypes, eventSymbols, onChange)
48 |
49 | subscribeTimeSeries = (
50 | eventTypes: EventType[],
51 | eventSymbols: string[],
52 | fromTime: number,
53 | onChange: (event: TEvent) => void
54 | ) => this.subscriptions.subscribeTimeSeries(eventTypes, eventSymbols, fromTime, onChange)
55 |
56 | /**
57 | * requires that incoming events have index, time and eventFlags to work correctly
58 | *
59 | * (!) expected that this method is not used alongside Feed.subscribeTimeSeries, it may not work correctly
60 | *
61 | * @param fromTime - A Number representing the milliseconds elapsed since the UNIX epoch
62 | * @param toTime - A Number representing the milliseconds elapsed since the UNIX epoch
63 | * @param options - default options has 15 seconds timeout
64 | */
65 | getTimeSeries = (
66 | eventSymbol: string,
67 | eventType: EventType,
68 | fromTime: number,
69 | toTime: number,
70 | options?: {
71 | signal?: AbortController['signal']
72 | timeoutMs?: number
73 | }
74 | ): Promise =>
75 | newPromiseWithResource((resolve, reject, useResource) => {
76 | useResource(() => {
77 | const timeoutId = setTimeout(() => {
78 | reject(new TimeoutError())
79 | }, options?.timeoutMs ?? 15_000)
80 | return () => clearTimeout(timeoutId)
81 | })
82 |
83 | useResource(() => {
84 | const abortSignalListener = () => reject(new AbortedError())
85 | options?.signal?.addEventListener('abort', abortSignalListener)
86 |
87 | return () => options?.signal?.removeEventListener('abort', abortSignalListener)
88 | })
89 |
90 | useResource(() => {
91 | const aggregator = newTimeSeriesAggregator(fromTime, toTime)
92 |
93 | const handleEvent = (event: TEvent) => {
94 | const result = aggregator.newEvent(event)
95 |
96 | if (isFinishedTimeSeriesAggregationResult(result)) resolve(result.events)
97 | }
98 |
99 | const unsubscribeTimeSeries = this.subscriptions.subscribeTimeSeries(
100 | [eventType],
101 | [eventSymbol],
102 | fromTime,
103 | handleEvent
104 | )
105 |
106 | return () => unsubscribeTimeSeries()
107 | })
108 | })
109 |
110 | /**
111 | * requires that incoming events have index, time and eventFlags to work correctly
112 | *
113 | * (!) expected that this method is not used alongside Feed.subscribeTimeSeries, it may not work correctly
114 | *
115 | * @param eventSymbol - A String representing the symbol of the event
116 | * @param eventType - A String representing the type of the event
117 | * @param fromTime - A Number representing the milliseconds elapsed since the UNIX epoch
118 | * @param onChange - A Function called when the snapshot is received
119 | * @returns - A Function that unsubscribes from the snapshot updates
120 | */
121 | subscribeTimeSeriesSnapshot = (
122 | eventSymbol: string,
123 | eventType: EventType,
124 | fromTime: number,
125 | onChange: (snapshot: IEvent[]) => void
126 | ) => {
127 | let snapshotPart = false // snapshot pending
128 | let snapshotFull = false // snapshot received in pending queue
129 | let tx = false // transaction pending
130 |
131 | let pQueue: IEvent[] = [] // pending queue
132 |
133 | let events: Record = {} // events accumulator
134 |
135 | return this.subscribeTimeSeries([eventType], [eventSymbol], fromTime, (event) => {
136 | const flags = parseEventFlags(event.eventFlags)
137 |
138 | tx = flags.txPending
139 |
140 | // Process snapshot start and clear params
141 | if (flags.snapshotBegin) {
142 | pQueue = [] // clear pending queue on new snapshot
143 | snapshotPart = true // snapshot pending
144 | snapshotFull = false // snapshot not received yet
145 | }
146 |
147 | // Process snapshot end after snapshot begin was received
148 | if (snapshotPart && (flags.snapshotEnd || flags.snapshotSnip)) {
149 | snapshotPart = false
150 | snapshotFull = true
151 | }
152 |
153 | pQueue.push(event)
154 |
155 | if (snapshotPart || tx) {
156 | return
157 | }
158 |
159 | if (snapshotFull) {
160 | snapshotFull = false
161 | events = {} // remove any unprocessed leftovers on new snapshot
162 | }
163 |
164 | // process pending queue
165 | let hasChanged = false
166 | for (const event of pQueue) {
167 | const { shouldBeRemoved } = parseEventFlags(event.eventFlags)
168 |
169 | if (shouldBeRemoved) {
170 | if (events[event.index] === undefined) {
171 | // nothing to do on remove on non-existing event
172 | continue
173 | }
174 |
175 | // remove existing event
176 | delete events[event.index]
177 | } else {
178 | // cleanup the flags in the stored event
179 | event.eventFlags = 0
180 | events[event.index] = event
181 | }
182 |
183 | hasChanged = true
184 | }
185 | pQueue = []
186 |
187 | if (hasChanged) {
188 | // notify about changes
189 | onChange(Object.values(events).sort((a, b) => a.index - b.index))
190 | }
191 | })
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import { Feed } from './feed'
9 |
10 | export * from './interfaces'
11 | export * from './eventFlags'
12 |
13 | export { Feed }
14 | export default Feed
15 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | export enum EventType {
9 | Quote = 'Quote',
10 | Candle = 'Candle',
11 | Trade = 'Trade',
12 | TradeETH = 'TradeETH',
13 | Summary = 'Summary',
14 | Profile = 'Profile',
15 | Greeks = 'Greeks',
16 | TheoPrice = 'TheoPrice',
17 | TimeAndSale = 'TimeAndSale',
18 | Underlying = 'Underlying',
19 | Order = 'Order',
20 | Configuration = 'Configuration',
21 | }
22 |
23 | export interface IEvent {
24 | eventType: EventType
25 | eventSymbol: string
26 | [key: string]: string | number | boolean
27 | }
28 |
29 | export interface ITimeSeriesEvent extends IEvent {
30 | index: number
31 | time: number
32 | eventFlags: number
33 | }
34 |
35 | export type CometdSeries = {
36 | [eventType in EventType]?: {
37 | [eventSymbol: string]: T
38 | }
39 | }
40 |
41 | export type ISubscriptionList = {
42 | [eventType in EventType]?: string[]
43 | }
44 |
45 | export type ITimeSeriesList = {
46 | [eventType in EventType]?: { fromTime: number; eventSymbol: string }[]
47 | }
48 |
49 | export type IncomingData = [
50 | EventType | [EventType, string[]], // head
51 | (string | number)[] // body
52 | ]
53 |
54 | export interface ITotalSubItem {
55 | listeners: ((event: IEvent) => void)[]
56 | }
57 |
58 | export interface ITotalTimeSeriesSubItem {
59 | listeners: ((event: IEvent) => void)[]
60 | fromTime: number
61 | fromTimes: number[]
62 | }
63 |
64 | export interface IFeedImplState {
65 | connected: boolean
66 | replaySupported?: boolean
67 | replay: boolean
68 | clear: boolean
69 | time: number
70 | speed: number
71 | [key: string]: IFeedImplState[keyof IFeedImplState]
72 | }
73 |
74 | export interface ISubscribeMessage {
75 | reset?: boolean
76 |
77 | add?: ISubscriptionList
78 | remove?: ISubscriptionList
79 | addTimeSeries?: ITimeSeriesList
80 | removeTimeSeries?: ISubscriptionList
81 | }
82 |
--------------------------------------------------------------------------------
/src/subscriptions.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import { Endpoint } from './endpoint'
9 | import {
10 | CometdSeries,
11 | EventType,
12 | IEvent,
13 | IFeedImplState,
14 | IncomingData,
15 | ISubscribeMessage,
16 | ITimeSeriesEvent,
17 | ITotalSubItem,
18 | ITotalTimeSeriesSubItem,
19 | } from './interfaces'
20 | import {
21 | isEmptySet,
22 | splitChunks,
23 | subMapToSetOfLists,
24 | timeSeriesSubMapToSetOfLists,
25 | toBooleanCometdSub,
26 | } from './utils'
27 |
28 | interface ITimeSeriesOptions {
29 | fromTime: number
30 | }
31 |
32 | type Queue = {
33 | reset: boolean
34 |
35 | count: number
36 |
37 | add: CometdSeries
38 | remove: CometdSeries
39 |
40 | addTimeSeries: CometdSeries
41 | removeTimeSeries: CometdSeries
42 | }
43 |
44 | const createDefaultQueue = (): Queue => ({
45 | reset: false,
46 |
47 | count: 0,
48 |
49 | add: {},
50 | remove: {},
51 |
52 | addTimeSeries: {},
53 | removeTimeSeries: {},
54 | })
55 |
56 | const MAX_QUEUE_SIZE = 200
57 |
58 | export class Subscriptions {
59 | endpoint: Endpoint
60 | sendSubTimeout: number | null = null
61 |
62 | schemeTypes: {
63 | [key: string]: string[]
64 | } = {}
65 |
66 | state: IFeedImplState = {
67 | connected: false,
68 | replaySupported: undefined, // will determined after connection
69 | replay: false,
70 | clear: false,
71 | time: 0,
72 | speed: 0,
73 | }
74 |
75 | subscriptions: CometdSeries = {}
76 | timeSeriesSubscriptions: CometdSeries = {}
77 |
78 | queue: Queue = createDefaultQueue()
79 |
80 | constructor(endpoint: Endpoint) {
81 | this.endpoint = endpoint
82 |
83 | endpoint.registerStateChangeHandler(this.onStateChange)
84 | endpoint.registerDataChangeHandler(this.onData)
85 | }
86 |
87 | sendSub = () => {
88 | const message: ISubscribeMessage = {}
89 |
90 | const queue = this.queue
91 | this.queue = createDefaultQueue()
92 |
93 | if (queue.reset) {
94 | message.reset = true
95 | queue.add = toBooleanCometdSub(this.subscriptions)
96 | queue.remove = {}
97 | queue.addTimeSeries = this.timeSeriesSubscriptions
98 | queue.removeTimeSeries = {}
99 | queue.reset = false
100 | }
101 |
102 | if (!isEmptySet(queue.add)) {
103 | message.add = subMapToSetOfLists(queue.add)
104 | }
105 | if (!isEmptySet(queue.remove)) {
106 | message.remove = subMapToSetOfLists(queue.remove)
107 | }
108 | if (!isEmptySet(queue.addTimeSeries)) {
109 | message.addTimeSeries = timeSeriesSubMapToSetOfLists(queue.addTimeSeries)
110 | }
111 | if (!isEmptySet(queue.removeTimeSeries)) {
112 | message.removeTimeSeries = subMapToSetOfLists(queue.removeTimeSeries)
113 | }
114 | if (!isEmptySet(message)) {
115 | this.endpoint.updateSubscriptions(message)
116 | }
117 | }
118 |
119 | scheduleSendSub() {
120 | // if queue is full, send it immediately
121 | if (this.queue.count > MAX_QUEUE_SIZE) {
122 | // Clear scheduled sendSub
123 | if (this.sendSubTimeout !== null) {
124 | clearTimeout(this.sendSubTimeout)
125 | this.sendSubTimeout = null
126 | }
127 | return this.sendSub()
128 | }
129 |
130 | if (this.sendSubTimeout === null) {
131 | this.sendSubTimeout = setTimeout(() => {
132 | this.sendSubTimeout = null
133 | this.sendSub()
134 | }, 0)
135 | }
136 | }
137 |
138 | subscribe(
139 | eventTypes: EventType[],
140 | eventSymbols: string[],
141 | onChange: (event: TEvent) => void
142 | ) {
143 | eventTypes.forEach((eventType) => {
144 | if (!this.subscriptions[eventType]) {
145 | this.subscriptions[eventType] = {}
146 | }
147 |
148 | eventSymbols.forEach((eventSymbol) => {
149 | if (!this.subscriptions[eventType][eventSymbol]) {
150 | this.subscriptions[eventType][eventSymbol] = {
151 | listeners: [],
152 | }
153 | }
154 |
155 | // Add listeners
156 | this.subscriptions[eventType][eventSymbol].listeners.push(onChange /* FIXME */ as any)
157 |
158 | /**
159 | * When backend receives message with `add` for the first time for symbol subscription, it immediately
160 | * pushes last ticker's value to a subscriber. The same is relevant for situation when backend receives
161 | * message with `remove`, and then, some ticks later, it receives another message with `add`.
162 | *
163 | * If `add` and `remove` are sent in one message, backend treats them in the following order: first remove
164 | * subscription, then add it back and push last value of ticker into it.
165 | *
166 | * When code below is not commented out, it deletes `remove` from message in situation
167 | * when `add` and `remove` occur in one tick. This makes backend treat message as "update subscription",
168 | * which effectively means "do nothing". New subscriber won't receive last value of ticker because subscription
169 | * already exists, and will only receive the next ticker update.
170 | *
171 | * This sometimes leads to cases when ticker appears empty for new subscribers of a rarely updated symbols.
172 | * (Related issue: EN-4718)
173 | */
174 |
175 | // if (this.queue.remove[eventType]) {
176 | // delete this.queue.remove[eventType][eventSymbol]
177 | // }
178 |
179 | this.queueAction('add', eventType, eventSymbol)
180 | })
181 | })
182 |
183 | // Return unsubscribe handler
184 | return () => {
185 | eventTypes.forEach((eventType) => {
186 | eventSymbols.forEach((eventSymbol) => {
187 | const subscription = this.subscriptions[eventType][eventSymbol]
188 |
189 | const newListeners = subscription.listeners.filter((listener) => listener !== onChange)
190 | if (newListeners.length === 0) {
191 | delete this.subscriptions[eventType][eventSymbol]
192 |
193 | // Remove from add queue
194 | if (this.queue.add[eventType]) {
195 | delete this.queue.add[eventType][eventSymbol]
196 | }
197 |
198 | this.queueAction('remove', eventType, eventSymbol)
199 | } else {
200 | subscription.listeners = newListeners
201 | }
202 | })
203 | })
204 | }
205 | }
206 |
207 | subscribeTimeSeries(
208 | eventTypes: EventType[],
209 | eventSymbols: string[],
210 | fromTime: number,
211 | onChange: (event: TEvent) => void
212 | ) {
213 | const handleEvent = (event: TEvent) => {
214 | if (event.time >= fromTime) {
215 | onChange(event)
216 | }
217 | }
218 |
219 | eventTypes.forEach((eventType) => {
220 | if (!this.timeSeriesSubscriptions[eventType]) {
221 | this.timeSeriesSubscriptions[eventType] = {}
222 | }
223 |
224 | eventSymbols.forEach((eventSymbol) => {
225 | if (!this.timeSeriesSubscriptions[eventType][eventSymbol]) {
226 | this.timeSeriesSubscriptions[eventType][eventSymbol] = {
227 | listeners: [],
228 | fromTime: Number.MAX_SAFE_INTEGER,
229 | fromTimes: [],
230 | }
231 | }
232 |
233 | // Add listeners
234 | const subscription = this.timeSeriesSubscriptions[eventType][eventSymbol]
235 | subscription.listeners.push(handleEvent /* FIXME */ as any)
236 | subscription.fromTimes.push(fromTime)
237 |
238 | // Delete from remove queue
239 | if (this.queue.removeTimeSeries[eventType]) {
240 | delete this.queue.removeTimeSeries[eventType][eventSymbol]
241 | }
242 |
243 | /*
244 | * Cases when incoming subscription timestamp is the same must trigger subscription too
245 | * (e.g. two simultaneous subscriptions coming from different clients)
246 | */
247 | if (fromTime <= subscription.fromTime) {
248 | subscription.fromTime = fromTime
249 |
250 | this.queueTimeSeriesAction('add', eventType, eventSymbol, {
251 | fromTime,
252 | })
253 | }
254 | })
255 | })
256 |
257 | // Return unsubscribe handler
258 | return () => {
259 | eventTypes.forEach((eventType) => {
260 | eventSymbols.forEach((eventSymbol) => {
261 | const subscription = this.timeSeriesSubscriptions[eventType][eventSymbol]
262 |
263 | // Remove time from list
264 | subscription.fromTimes.splice(subscription.fromTimes.indexOf(fromTime), 1)
265 |
266 | const newListeners = subscription.listeners.filter((listener) => listener !== handleEvent)
267 | if (newListeners.length === 0) {
268 | delete this.timeSeriesSubscriptions[eventType][eventSymbol]
269 |
270 | // Remove from add queue
271 | if (this.queue.addTimeSeries[eventType]) {
272 | delete this.queue.addTimeSeries[eventType][eventSymbol]
273 | }
274 |
275 | this.queueTimeSeriesAction('remove', eventType, eventSymbol)
276 | } else {
277 | subscription.listeners = newListeners
278 |
279 | const newFromTime = subscription.fromTimes.reduce(
280 | (result, time) => (fromTime < result ? time : result),
281 | Number.POSITIVE_INFINITY
282 | )
283 | if (subscription.fromTime !== newFromTime) {
284 | subscription.fromTime = newFromTime
285 |
286 | this.queueTimeSeriesAction('add', eventType, eventSymbol, {
287 | fromTime: newFromTime,
288 | })
289 | }
290 | }
291 | })
292 | })
293 | }
294 | }
295 |
296 | private onStateChange = (stateChange: Partial) => {
297 | if (stateChange.connected) {
298 | this.queue.reset = true
299 | this.scheduleSendSub()
300 | }
301 |
302 | Object.entries(stateChange).forEach(([key, val]) => {
303 | this.state[key] = val
304 | })
305 | }
306 |
307 | private queueAction = (action: 'add' | 'remove', eventType: EventType, eventSymbol: string) => {
308 | this.queue[action] = {
309 | ...this.queue[action],
310 | [eventType]: {
311 | ...this.queue[action][eventType],
312 | [eventSymbol]: true,
313 | },
314 | }
315 |
316 | this.queue.count++
317 |
318 | this.scheduleSendSub()
319 | }
320 |
321 | private queueTimeSeriesAction = (
322 | action: 'add' | 'remove',
323 | eventType: EventType,
324 | eventSymbol: string,
325 | options?: ITimeSeriesOptions
326 | ) => {
327 | if (action === 'add') {
328 | this.queue.addTimeSeries = {
329 | ...this.queue.addTimeSeries,
330 | [eventType]: {
331 | ...this.queue.addTimeSeries[eventType],
332 | [eventSymbol]: options,
333 | },
334 | }
335 | } else {
336 | this.queue.removeTimeSeries = {
337 | ...this.queue.removeTimeSeries,
338 | [eventType]: {
339 | ...this.queue.removeTimeSeries[eventType],
340 | [eventSymbol]: true,
341 | },
342 | }
343 | }
344 |
345 | this.queue.count++
346 |
347 | this.scheduleSendSub()
348 | }
349 |
350 | private onData = ([headData, bodyData]: IncomingData, timeSeries: boolean) => {
351 | const subscriptions = timeSeries ? this.timeSeriesSubscriptions : this.subscriptions
352 |
353 | let eventType: EventType
354 | let scheme: string[]
355 |
356 | if (typeof headData === 'string') {
357 | eventType = headData
358 | scheme = this.schemeTypes[eventType]
359 | } else {
360 | ;[eventType, scheme] = headData
361 |
362 | this.schemeTypes[eventType] = scheme
363 | }
364 |
365 | const subscription = subscriptions[eventType]
366 | if (!subscription) {
367 | return
368 | }
369 |
370 | const eventsValues = splitChunks(bodyData, scheme.length)
371 | eventsValues.forEach((values) => {
372 | const event: IEvent = {
373 | eventType,
374 | eventSymbol: '',
375 | }
376 |
377 | scheme.forEach((eventPropertyName, eventPropertyIndex) => {
378 | event[eventPropertyName] = values[eventPropertyIndex]
379 | })
380 |
381 | subscription[event.eventSymbol]?.listeners.forEach((listener) => listener(event))
382 | })
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/src/timeSeriesAggregator.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import { parseEventFlags } from './eventFlags'
9 | import { ITimeSeriesEvent } from './interfaces'
10 |
11 | // AggregationEvent
12 | type AggregationEvent = TEvent & {
13 | time: number
14 | /**
15 | * unique event identifier
16 | */
17 | index: number
18 | eventFlags: number
19 | }
20 |
21 | const isAggregationEvent = (
22 | event: TEvent
23 | ): event is AggregationEvent =>
24 | typeof event.time === 'number' &&
25 | typeof event.index === 'number' &&
26 | typeof event.eventFlags === 'number'
27 |
28 | // TimeSeriesAggregationResult
29 | export interface ContinueTimeSeriesAggregationResult {
30 | kind: 'Continue'
31 | }
32 |
33 | export interface FinishedTimeSeriesAggregationResult {
34 | kind: 'Finished'
35 | events: TEvent[]
36 | }
37 |
38 | export type TimeSeriesAggregationResult =
39 | | ContinueTimeSeriesAggregationResult
40 | | FinishedTimeSeriesAggregationResult
41 |
42 | export const isFinishedTimeSeriesAggregationResult = (
43 | result: TimeSeriesAggregationResult
44 | ): result is FinishedTimeSeriesAggregationResult => result.kind === 'Finished'
45 |
46 | // TimeSeriesAggregator
47 | export interface TimeSeriesAggregator {
48 | newEvent: (event: TEvent) => TimeSeriesAggregationResult
49 | }
50 |
51 | export const newTimeSeriesAggregator = (
52 | fromTime: number,
53 | toTime: number
54 | ): TimeSeriesAggregator => {
55 | let complete = false
56 | let txPending = false
57 | const events: Record> = {}
58 |
59 | const isDone = () => complete && !txPending
60 |
61 | const updateEvents = (event: AggregationEvent, remove: boolean) => {
62 | if (remove) {
63 | delete events[event.index]
64 | } else {
65 | events[event.index] = event
66 | }
67 | }
68 |
69 | const processEvent = (event: TEvent) => {
70 | if (!isAggregationEvent(event)) {
71 | return
72 | }
73 |
74 | const time = event.time
75 | const flags = parseEventFlags(event.eventFlags)
76 | txPending = flags.txPending
77 |
78 | if (time >= fromTime && time <= toTime) {
79 | const remove = flags.shouldBeRemoved
80 | event.eventFlags = 0
81 | updateEvents(event, remove)
82 | }
83 | if (time <= fromTime || flags.snapshotSnip) complete = true
84 | }
85 |
86 | const newEvent = (event: TEvent): TimeSeriesAggregationResult => {
87 | processEvent(event)
88 | return isDone()
89 | ? { kind: 'Finished', events: Object.values(events).sort((a, b) => a.index - b.index) }
90 | : { kind: 'Continue' }
91 | }
92 |
93 | return {
94 | newEvent,
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /** @license
2 | * Copyright ©2020 Devexperts LLC. All rights reserved.
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
5 | * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 | */
7 |
8 | import { CometdSeries, EventType, ISubscriptionList } from './interfaces'
9 |
10 | export function subMapToSetOfLists(sub: CometdSeries): ISubscriptionList {
11 | // sub : (type :-> symbol :-> subItem), returns (type :-> [symbol])
12 | return Object.entries(sub).reduce((acc, value) => {
13 | acc[value[0] as EventType] = Object.keys(value[1])
14 | return acc
15 | }, {} as ISubscriptionList)
16 | }
17 |
18 | export const toBooleanCometdSub = (sub: CometdSeries) =>
19 | Object.keys(sub).reduce((acc, key) => {
20 | const eventType = key as EventType
21 | acc[eventType] = Object.keys(sub[eventType]).reduce((innerAcc, innerKey) => {
22 | innerAcc[innerKey] = Boolean(sub[eventType][innerKey])
23 | return innerAcc
24 | }, {} as Record)
25 | return acc
26 | }, {} as CometdSeries)
27 |
28 | function timeSeriesSubSetToList(obj: Record) {
29 | return Object.keys(obj).map((key) => ({
30 | eventSymbol: key,
31 | fromTime: obj[key].fromTime,
32 | }))
33 | }
34 |
35 | export function timeSeriesSubMapToSetOfLists(sub: CometdSeries<{ fromTime: number }>) {
36 | return Object.entries(sub).reduce((acc, value) => {
37 | acc[value[0]] = timeSeriesSubSetToList(value[1])
38 | return acc
39 | }, {} as Record)
40 | }
41 |
42 | export const isEmptySet = (obj: object) => Object.keys(obj).length === 0
43 |
44 | export const splitChunks = (values: Value[], chunkSize: number) => {
45 | const results: Value[][] = []
46 |
47 | for (let offset = 0; offset < values.length; offset += chunkSize) {
48 | results.push(values.slice(offset, offset + chunkSize))
49 | }
50 |
51 | return results
52 | }
53 |
54 | type Cleanup = () => void
55 | /**
56 | * regular promise creator but with the ability to handle code that requires cleanup in a simpler way
57 | *
58 | * @example
59 | * // setup promise that will be rejected after 5 seconds
60 | * newPromiseWithResource((resolve, reject, useResource) => {
61 | * useResource(() => {
62 | * const id = setTimeout(() => reject('Timeout'), 5_000)
63 | * return () => clearTimeout(id)
64 | * })
65 | * // additional body steps, like http requests
66 | * })
67 | */
68 | export const newPromiseWithResource = (
69 | executor: (
70 | resolve: (value: T) => void,
71 | reject: (reason: unknown) => void,
72 | useResource: (resourceFn: () => Cleanup) => void
73 | ) => void
74 | ): Promise => {
75 | const cleanups: Cleanup[] = []
76 |
77 | const cleanupAll = () => {
78 | cleanups.forEach((cleanup) => cleanup())
79 | }
80 |
81 | return new Promise((resolve, reject) => {
82 | const useResource = (resourceFn: () => Cleanup) => {
83 | const cleanup = resourceFn()
84 | cleanups.push(cleanup)
85 | }
86 |
87 | const resolveWithCleanup = (value: T) => {
88 | cleanupAll()
89 | resolve(value)
90 | }
91 | const rejectWithCleanup = (reason: unknown) => {
92 | cleanupAll()
93 | reject(reason)
94 | }
95 |
96 | executor(resolveWithCleanup, rejectWithCleanup, useResource)
97 | })
98 | }
99 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "allowUnusedLabels": true,
5 | "baseUrl": ".",
6 | "experimentalDecorators": true,
7 | "esModuleInterop": true,
8 | "declaration": true,
9 | "lib": ["dom", "es2015", "esnext"],
10 | "module": "commonjs",
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "types": ["jest"],
14 | "removeComments": false,
15 | "inlineSourceMap": true,
16 | "strict": true,
17 | "strictNullChecks": false,
18 | "skipLibCheck": true,
19 | "target": "es2017"
20 | },
21 | "include": ["src"],
22 | "exclude": ["node_modules", "*/__mocks__", "*/**/*.test.ts"]
23 | }
24 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-config-prettier"],
3 | "jsRules": {
4 | "object-literal-sort-keys": [false]
5 | },
6 | "rules": {
7 | "ordered-imports": [
8 | true,
9 | {
10 | "import-sources-order": "lowercase-first",
11 | "module-source-path": "full",
12 | "grouped-imports": true,
13 | "groups": [
14 | {
15 | "match": "^react",
16 | "order": 1
17 | },
18 | {
19 | "match": "^@/(components|containers)",
20 | "order": 31
21 | },
22 | {
23 | "match": "^@/",
24 | "order": 20
25 | },
26 | {
27 | "name": "Parent dir",
28 | "match": "^[.][.]",
29 | "order": 100
30 | },
31 | {
32 | "name": "Current dir",
33 | "match": "^[.]",
34 | "order": 110
35 | },
36 | {
37 | "match": "^[^\\.]",
38 | "order": 10
39 | }
40 | ]
41 | }
42 | ],
43 | "object-literal-sort-keys": [false],
44 | "arrow-return-shorthand": [true, "multiline"],
45 | "variable-name": {
46 | "options": ["ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"]
47 | },
48 | "no-duplicate-imports": [true],
49 | "no-return-await": true,
50 | "jsx-boolean-value": [false],
51 | "jsx-no-lambda": [false],
52 | "no-default-export": [false],
53 | "member-access": [true, "no-public"],
54 | "file-name-casing": [
55 | true,
56 | {
57 | "index.tsx": "camel-case",
58 | "stories.tsx": "camel-case",
59 | ".tsx": "pascal-case",
60 | ".ts": "camel-case"
61 | }
62 | ]
63 | }
64 | }
65 |
--------------------------------------------------------------------------------