Skip to content

Commit 828afd1

Browse files
authored
feat(nodejs): introduce safer timestamp API (#21)
* feat(nodejs): safer timestamp API * Update CI configs * Address review comments
1 parent 4ba7774 commit 828afd1

35 files changed

+3673
-3091
lines changed

.github/workflows/build.yml

+7-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
name: build
22

3-
on:
4-
push:
3+
on: [push]
54

65
jobs:
7-
build:
8-
name: Build @questdb/nodejs-questdb-client
6+
test:
7+
name: Build with Node.js ${{ matrix.node-version }}
98
runs-on: ubuntu-latest
9+
strategy:
10+
matrix:
11+
node-version: [16, 20]
1012
steps:
1113
- name: Checkout repository
1214
uses: actions/checkout@v3
@@ -16,7 +18,7 @@ jobs:
1618
- name: Setup node
1719
uses: actions/setup-node@v3
1820
with:
19-
node-version: 16
21+
node-version: ${{ matrix.node-version }}
2022

2123
- name: Install dependencies
2224
run: npm ci
@@ -26,12 +28,3 @@ jobs:
2628

2729
- name: Run linter
2830
run: npm run eslint
29-
30-
- name: Publish @questdb/nodejs-questdb-client to npm
31-
if: github.ref == 'refs/heads/main'
32-
uses: JS-DevTools/npm-publish@v1
33-
with:
34-
token: ${{ secrets.CI_TOKEN }}
35-
access: public
36-
check-version: true
37-
package: package.json

.github/workflows/publish.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: build
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build:
10+
name: Publish @questdb/nodejs-questdb-client to npm
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v3
15+
with:
16+
submodules: recursive
17+
18+
- name: Setup node
19+
uses: actions/setup-node@v3
20+
with:
21+
node-version: 20
22+
23+
- name: Install dependencies
24+
run: npm ci
25+
26+
- name: Run tests
27+
run: npm test
28+
29+
- name: Run linter
30+
run: npm run eslint
31+
32+
- name: Publish
33+
uses: JS-DevTools/npm-publish@v2
34+
with:
35+
token: ${{ secrets.CI_TOKEN }}
36+
access: public
37+
strategy: all
38+
package: package.json

README.md

+66-52
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,143 @@
1-
## QuestDB Node.js Client
1+
# QuestDB Node.js Client
2+
3+
## Requirements
4+
5+
The client requires Node.js v16 or newer version.
26

37
## Installation
48
```shell
5-
npm install @questdb/nodejs-client
9+
npm i -s @questdb/nodejs-client
610
```
711

812
## Examples
913

1014
### Basic API usage
15+
1116
```javascript
12-
const { Sender } = require("@questdb/nodejs-client");
17+
const { Sender } = require('@questdb/nodejs-client');
1318

1419
async function run() {
15-
// create a sender with a 4k buffer
16-
// it is important to size the buffer correctly so messages can fit
17-
const sender = new Sender({bufferSize: 4096});
20+
const sender = new Sender();
1821

1922
// connect to QuestDB
2023
// host and port are required in connect options
21-
await sender.connect({port: 9009, host: "localhost"});
24+
await sender.connect({port: 9009, host: 'localhost'});
2225

2326
// add rows to the buffer of the sender
24-
sender.table("prices").symbol("instrument", "EURUSD")
25-
.floatColumn("bid", 1.0195).floatColumn("ask", 1.0221).atNow();
26-
sender.table("prices").symbol("instrument", "GBPUSD")
27-
.floatColumn("bid", 1.2076).floatColumn("ask", 1.2082).atNow();
27+
sender.table('prices').symbol('instrument', 'EURUSD')
28+
.floatColumn('bid', 1.0195).floatColumn('ask', 1.0221)
29+
.at(Date.now(), 'ms');
30+
sender.table('prices').symbol('instrument', 'GBPUSD')
31+
.floatColumn('bid', 1.2076).floatColumn('ask', 1.2082)
32+
.at(Date.now(), 'ms');
2833

2934
// flush the buffer of the sender, sending the data to QuestDB
3035
// the buffer is cleared after the data is sent and the sender is ready to accept new data
3136
await sender.flush();
3237

3338
// add rows to the buffer again and send it to the server
34-
sender.table("prices").symbol("instrument", "EURUSD")
35-
.floatColumn("bid", 1.0197).floatColumn("ask", 1.0224).atNow();
39+
sender.table('prices').symbol('instrument', 'EURUSD')
40+
.floatColumn('bid', 1.0197).floatColumn('ask', 1.0224)
41+
.at(Date.now(), 'ms');
3642
await sender.flush();
3743

3844
// close the connection after all rows ingested
3945
await sender.close();
4046
return new Promise(resolve => resolve(0));
4147
}
4248

43-
run().then(value => console.log(value)).catch(err => console.log(err));
49+
run()
50+
.then(console.log)
51+
.catch(console.error);
4452
```
4553

4654
### Authentication and secure connection
55+
4756
```javascript
48-
const { Sender } = require("@questdb/nodejs-client");
57+
const { Sender } = require('@questdb/nodejs-client');
4958

5059
async function run() {
5160
// construct a JsonWebKey
52-
const CLIENT_ID = "testapp";
53-
const PRIVATE_KEY = "9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8";
61+
const CLIENT_ID = 'testapp';
62+
const PRIVATE_KEY = '9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8';
5463
const PUBLIC_KEY = {
55-
x: "aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc",
56-
y: "__ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg"
64+
x: 'aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc',
65+
y: '__ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg'
5766
};
5867
const JWK = {
5968
...PUBLIC_KEY,
6069
d: PRIVATE_KEY,
6170
kid: CLIENT_ID,
62-
kty: "EC",
63-
crv: "P-256",
71+
kty: 'EC',
72+
crv: 'P-256',
6473
};
6574

6675
// pass the JsonWebKey to the sender
6776
// will use it for authentication
68-
const sender = new Sender({bufferSize: 4096, jwk: JWK});
77+
const sender = new Sender({jwk: JWK});
6978

7079
// connect() takes an optional second argument
7180
// if 'true' passed the connection is secured with TLS encryption
72-
await sender.connect({port: 9009, host: "localhost"}, true);
81+
await sender.connect({port: 9009, host: 'localhost'}, true);
7382

7483
// send the data over the authenticated and secure connection
75-
sender.table("prices").symbol("instrument", "EURUSD")
76-
.floatColumn("bid", 1.0197).floatColumn("ask", 1.0224).atNow();
84+
sender.table('prices').symbol('instrument', 'EURUSD')
85+
.floatColumn('bid', 1.0197).floatColumn('ask', 1.0224)
86+
.at(Date.now(), 'ms');
7787
await sender.flush();
7888

7989
// close the connection after all rows ingested
8090
await sender.close();
81-
return new Promise(resolve => resolve(0));
8291
}
8392

84-
run().then(value => console.log(value)).catch(err => console.log(err));
93+
run().catch(console.error);
8594
```
8695

8796
### TypeScript example
97+
8898
```typescript
89-
import { Sender } from "@questdb/nodejs-client";
99+
import { Sender } from '@questdb/nodejs-client';
90100

91101
async function run(): Promise<number> {
92102
// construct a JsonWebKey
93-
const CLIENT_ID: string = "testapp";
94-
const PRIVATE_KEY: string = "9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8";
103+
const CLIENT_ID: string = 'testapp';
104+
const PRIVATE_KEY: string = '9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8';
95105
const PUBLIC_KEY: { x: string, y: string } = {
96-
x: "aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc",
97-
y: "__ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg"
106+
x: 'aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc',
107+
y: '__ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg'
98108
};
99109
const JWK: { x: string, y: string, kid: string, kty: string, d: string, crv: string } = {
100110
...PUBLIC_KEY,
101111
d: PRIVATE_KEY,
102112
kid: CLIENT_ID,
103-
kty: "EC",
104-
crv: "P-256",
113+
kty: 'EC',
114+
crv: 'P-256',
105115
};
106116

107117
// pass the JsonWebKey to the sender
108118
// will use it for authentication
109-
const sender: Sender = new Sender({bufferSize: 4096, jwk: JWK});
119+
const sender: Sender = new Sender({jwk: JWK});
110120

111121
// connect() takes an optional second argument
112122
// if 'true' passed the connection is secured with TLS encryption
113-
await sender.connect({port: 9009, host: "localhost"}, true);
123+
await sender.connect({port: 9009, host: 'localhost'}, true);
114124

115125
// send the data over the authenticated and secure connection
116-
sender.table("prices").symbol("instrument", "EURUSD")
117-
.floatColumn("bid", 1.0197).floatColumn("ask", 1.0224).atNow();
126+
sender.table('prices').symbol('instrument', 'EURUSD')
127+
.floatColumn('bid', 1.0197).floatColumn('ask', 1.0224).at(Date.now(), 'ms');
118128
await sender.flush();
119129

120130
// close the connection after all rows ingested
121131
await sender.close();
122-
return new Promise(resolve => resolve(0));
123132
}
124133

125-
run().then(value => console.log(value)).catch(err => console.log(err));
134+
run().catch(console.error);
126135
```
127136

128137
### Worker threads example
138+
129139
```javascript
130-
const { Sender } = require("@questdb/nodejs-client");
140+
const { Sender } = require('@questdb/nodejs-client');
131141
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
132142

133143
// fake venue
@@ -136,7 +146,7 @@ function* venue(ticker) {
136146
let end = false;
137147
setTimeout(() => { end = true; }, rndInt(5000));
138148
while (!end) {
139-
yield {"ticker": ticker, "price": Math.random()};
149+
yield {'ticker': ticker, 'price': Math.random()};
140150
}
141151
}
142152

@@ -153,35 +163,37 @@ async function subscribe(ticker, onTick) {
153163

154164
async function run() {
155165
if (isMainThread) {
156-
const tickers = ["t1", "t2", "t3", "t4"];
166+
const tickers = ['t1', 't2', 't3', 't4'];
157167
// main thread to start a worker thread for each ticker
158168
for (let ticker in tickers) {
159169
const worker = new Worker(__filename, { workerData: { ticker: ticker } })
160170
.on('error', (err) => { throw err; })
161171
.on('exit', () => { console.log(`${ticker} thread exiting...`); })
162-
.on('message', (msg) => { console.log("Ingested " + msg.count + " prices for ticker " + msg.ticker); });
172+
.on('message', (msg) => {
173+
console.log(`Ingested ${msg.count} prices for ticker ${msg.ticker}`);
174+
});
163175
}
164176
} else {
165177
// it is important that each worker has a dedicated sender object
166178
// threads cannot share the sender because they would write into the same buffer
167-
const sender = new Sender({ bufferSize: 4096 });
168-
await sender.connect({ port: 9009, host: "localhost" });
179+
const sender = new Sender();
180+
await sender.connect({ port: 9009, host: 'localhost' });
169181

170182
// subscribe for the market data of the ticker assigned to the worker
171183
// ingest each price update into the database using the sender
172184
let count = 0;
173185
await subscribe(workerData.ticker, async (tick) => {
174186
sender
175-
.table("prices")
176-
.symbol("ticker", tick.ticker)
177-
.floatColumn("price", tick.price)
178-
.atNow();
187+
.table('prices')
188+
.symbol('ticker', tick.ticker)
189+
.floatColumn('price', tick.price)
190+
.at(Date.now(), 'ms');
179191
await sender.flush();
180192
count++;
181193
});
182194

183195
// let the main thread know how many prices were ingested
184-
parentPort.postMessage({"ticker": workerData.ticker, "count": count});
196+
parentPort.postMessage({'ticker': workerData.ticker, 'count': count});
185197

186198
// close the connection to the database
187199
await sender.close();
@@ -196,5 +208,7 @@ function rndInt(limit) {
196208
return Math.floor((Math.random() * limit) + 1);
197209
}
198210

199-
run().catch((err) => console.log(err));
211+
run()
212+
.then(console.log)
213+
.catch(console.error);
200214
```

0 commit comments

Comments
 (0)