Skip to content

Commit 411fb00

Browse files
committed
feat: implement an async iterable request class
1 parent 3856dc5 commit 411fb00

File tree

4 files changed

+384
-1
lines changed

4 files changed

+384
-1
lines changed

src/iterable-request.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// This module implements an iterable `Request` class.
2+
3+
import Request, { type RequestOptions } from './request';
4+
import { type ColumnMetadata } from './token/colmetadata-token-parser';
5+
6+
export interface ColumnValue {
7+
metadata: ColumnMetadata;
8+
value: any;
9+
}
10+
11+
type RowData = ColumnValue[] | Record<string, ColumnValue>; // type variant depending on config.options.useColumnNames
12+
type ColumnMetadataDef = ColumnMetadata[] | Record<string, ColumnMetadata>; // type variant depending on config.options.useColumnNames
13+
14+
export interface IterableRequestOptions extends RequestOptions {
15+
iteratorFifoSize: number;
16+
}
17+
18+
/**
19+
* The item object of the request iterator.
20+
*/
21+
export interface IterableRequestItem {
22+
23+
/**
24+
* Row data.
25+
*/
26+
row: RowData;
27+
28+
/**
29+
* Result set number, 1..n.
30+
*/
31+
resultSetNo: number;
32+
33+
/**
34+
* Metadata of all columns.
35+
*/
36+
columnMetadata: ColumnMetadataDef;
37+
}
38+
39+
type iteratorPromiseResolveFunction = (value: IteratorResult<IterableRequestItem>) => void;
40+
type iteratorPromiseRejectFunction = (error: Error) => void;
41+
42+
// Internal class for the state controller logic of the iterator.
43+
class IterableRequestController {
44+
45+
private request: Request;
46+
private requestCompleted: boolean;
47+
private requestPaused: boolean;
48+
private error: Error | undefined;
49+
private terminating: boolean;
50+
51+
private resultSetNo: number;
52+
private columnMetadata: ColumnMetadataDef | undefined;
53+
private fifo: IterableRequestItem[];
54+
private fifoPauseLevel: number;
55+
private fifoResumeLevel: number;
56+
57+
private promisePending: boolean;
58+
private resolvePromise: iteratorPromiseResolveFunction | undefined;
59+
private rejectPromise: iteratorPromiseRejectFunction | undefined;
60+
private terminatorResolve: (() => void) | undefined;
61+
62+
// --- Constructor / Terminator ----------------------------------------------
63+
64+
constructor(request: Request, options?: IterableRequestOptions) {
65+
this.request = request;
66+
this.requestCompleted = false;
67+
this.requestPaused = false;
68+
this.terminating = false;
69+
70+
this.resultSetNo = 0;
71+
this.fifo = [];
72+
const fifoSize = options?.iteratorFifoSize ?? 1024;
73+
this.fifoPauseLevel = fifoSize;
74+
this.fifoResumeLevel = Math.floor(fifoSize / 2);
75+
76+
this.promisePending = false;
77+
78+
request.addListener('row', this.rowEventHandler);
79+
request.addListener('columnMetadata', this.columnMetadataEventHandler);
80+
}
81+
82+
public terminate(): Promise<void> {
83+
this.terminating = true;
84+
this.request.resume(); // (just to be sure)
85+
if (this.requestCompleted || !this.request.connection) {
86+
return Promise.resolve();
87+
}
88+
this.request.connection.cancel();
89+
return new Promise<void>((resolve: () => void) => {
90+
this.terminatorResolve = resolve;
91+
});
92+
}
93+
94+
// --- Promise logic ---------------------------------------------------------
95+
96+
private serveError(): boolean {
97+
if (!this.error || !this.promisePending) {
98+
return false;
99+
}
100+
this.rejectPromise!(this.error);
101+
this.promisePending = false;
102+
return true;
103+
}
104+
105+
private serveRowItem(): boolean {
106+
if (!this.fifo.length || !this.promisePending) {
107+
return false;
108+
}
109+
const item = this.fifo.shift()!;
110+
this.resolvePromise!({ value: item });
111+
this.promisePending = false;
112+
if (this.fifo.length <= this.fifoResumeLevel && this.requestPaused) {
113+
this.request.resume();
114+
this.requestPaused = false;
115+
}
116+
return true;
117+
}
118+
119+
private serveRequestCompletion(): boolean {
120+
if (!this.requestCompleted || !this.promisePending) {
121+
return false;
122+
}
123+
this.resolvePromise!({ done: true, value: undefined });
124+
this.promisePending = false;
125+
return true;
126+
}
127+
128+
private servePromise() {
129+
if (this.serveError()) {
130+
return;
131+
}
132+
if (this.serveRowItem()) {
133+
return;
134+
}
135+
if (this.serveRequestCompletion()) {
136+
return; // eslint-disable-line no-useless-return
137+
}
138+
}
139+
140+
// This promise executor is called synchronously from within Iterator.next().
141+
public promiseExecutor = (resolve: iteratorPromiseResolveFunction, reject: iteratorPromiseRejectFunction) => {
142+
if (this.promisePending) {
143+
throw new Error('Previous promise is still active.');
144+
}
145+
this.resolvePromise = resolve;
146+
this.rejectPromise = reject;
147+
this.promisePending = true;
148+
this.servePromise();
149+
};
150+
151+
// --- Event handlers --------------------------------------------------------
152+
153+
public completionCallback(error: Error | null | undefined) {
154+
this.requestCompleted = true;
155+
if (this.terminating) {
156+
if (this.terminatorResolve) {
157+
this.terminatorResolve();
158+
}
159+
return;
160+
}
161+
if (error && !this.error) {
162+
this.error = error;
163+
}
164+
this.servePromise();
165+
}
166+
167+
private columnMetadataEventHandler = (columnMetadata: ColumnMetadata[] | Record<string, ColumnMetadata>) => {
168+
this.resultSetNo++;
169+
this.columnMetadata = columnMetadata;
170+
};
171+
172+
private rowEventHandler = (row: RowData) => {
173+
if (this.requestCompleted || this.error || this.terminating) {
174+
return;
175+
}
176+
if (this.resultSetNo === 0 || !this.columnMetadata) {
177+
this.error = new Error('No columnMetadata event received before row event.');
178+
this.servePromise();
179+
return;
180+
}
181+
const item: IterableRequestItem = { row, resultSetNo: this.resultSetNo, columnMetadata: this.columnMetadata };
182+
this.fifo.push(item);
183+
if (this.fifo.length >= this.fifoPauseLevel && !this.requestPaused) {
184+
this.request.pause();
185+
this.requestPaused = true;
186+
}
187+
this.servePromise();
188+
};
189+
190+
}
191+
192+
// Internal class for the iterator object which is passed to the API client.
193+
class IterableRequestIterator implements AsyncIterator<IterableRequestItem> {
194+
195+
private controller: IterableRequestController;
196+
197+
constructor(controller: IterableRequestController) {
198+
this.controller = controller;
199+
}
200+
201+
public next(): Promise<IteratorResult<IterableRequestItem>> {
202+
return new Promise<IteratorResult<IterableRequestItem>>(this.controller.promiseExecutor);
203+
}
204+
205+
public async return(value?: any): Promise<any> {
206+
await this.controller.terminate();
207+
return Promise.resolve({ value, done: true }); // eslint-disable-line @typescript-eslint/return-await
208+
}
209+
210+
public async throw(_exception?: any): Promise<any> {
211+
await this.controller.terminate();
212+
return Promise.resolve({ done: true }); // eslint-disable-line @typescript-eslint/return-await
213+
}
214+
215+
}
216+
217+
/**
218+
* An iterable `Request` class.
219+
*
220+
* This iterable version is a super class of the normal `Request` class.
221+
*
222+
* Usage:
223+
* ```js
224+
* const request = new IterableRequest("select 42, 'hello world'");
225+
* connection.execSql(request);
226+
* for await (const item of request) {
227+
* console.log(item.row);
228+
* }
229+
* ```
230+
*/
231+
class IterableRequest extends Request implements AsyncIterable<IterableRequestItem> {
232+
233+
private iterator: IterableRequestIterator;
234+
235+
constructor(sqlTextOrProcedure: string | undefined, options?: IterableRequestOptions) {
236+
super(sqlTextOrProcedure, completionCallback, options);
237+
const controller = new IterableRequestController(this, options);
238+
this.iterator = new IterableRequestIterator(controller);
239+
240+
function completionCallback(error: Error | null | undefined) {
241+
if (controller) {
242+
controller.completionCallback(error);
243+
}
244+
}
245+
}
246+
247+
[Symbol.asyncIterator](): AsyncIterator<IterableRequestItem> {
248+
return this.iterator;
249+
}
250+
251+
}
252+
253+
export default IterableRequest;
254+
module.exports = IterableRequest;

src/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface ParameterOptions {
3939
scale?: number;
4040
}
4141

42-
interface RequestOptions {
42+
export interface RequestOptions {
4343
statementColumnEncryptionSetting?: SQLServerStatementColumnEncryptionSetting;
4444
}
4545

src/tedious.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import BulkLoad from './bulk-load';
22
import Connection, { type ConnectionAuthentication, type ConnectionConfiguration, type ConnectionOptions } from './connection';
33
import Request from './request';
4+
import IterableRequest from './iterable-request';
45
import { name } from './library';
56

67
import { ConnectionError, RequestError } from './errors';
@@ -21,6 +22,7 @@ export {
2122
BulkLoad,
2223
Connection,
2324
Request,
25+
IterableRequest,
2426
library,
2527
ConnectionError,
2628
RequestError,

0 commit comments

Comments
 (0)