@sqb/connect
Version:
Multi-dialect database connection framework written with TypeScript
257 lines (256 loc) • 7.53 kB
JavaScript
import _debug from 'debug';
import DoublyLinked from 'doublylinked';
import { TaskQueue } from 'power-tasks';
import { coerceToInt } from 'putil-varhelpers';
import { AsyncEventEmitter, TypedEventEmitterClass } from 'strict-typed-events';
import { CursorStream } from './cursor-stream.js';
import { callFetchHooks, normalizeRowsToArrayRows, normalizeRowsToObjectRows, } from './helpers.js';
const debug = _debug('sqb:cursor');
export class Cursor extends TypedEventEmitterClass(AsyncEventEmitter) {
_connection;
_fields;
_prefetchRows;
_request;
_intlcur;
_taskQueue = new TaskQueue();
_fetchCache = new DoublyLinked();
_rowNum = 0;
_fetchedAll = false;
_fetchedRows = 0;
_row;
_cache;
constructor(connection, fields, adapterCursor, request) {
super();
this._connection = connection;
this._intlcur = adapterCursor;
this._fields = fields;
this._request = request;
this._prefetchRows = request?.fetchRows || 100;
}
/**
* Returns the Connection instance
*/
get connection() {
return this._connection;
}
/**
* Returns if cursor is before first record.
*/
get isBof() {
return !this._rowNum;
}
/**
* Returns if cursor is closed.
*/
get isClosed() {
return !this._intlcur;
}
/**
* Returns if cursor is after last record.
*/
get isEof() {
return this._fetchedAll && this.rowNum > this._fetchedRows;
}
/**
* Returns number of fetched record count from database.
*/
get fetchedRows() {
return this._fetchedRows;
}
/**
* Returns object instance which contains information about fields.
*/
get fields() {
return this._fields;
}
/**
* Returns current row
*/
get row() {
return this._row;
}
/**
* Returns current row number.
*/
get rowNum() {
return this._rowNum;
}
/**
* Enables cache
*/
cached() {
if (this.fetchedRows)
throw new Error('Cache can be enabled before fetching rows');
if (!this._cache)
this._cache = new DoublyLinked();
}
/**
* Closes cursor
*/
async close() {
if (!this._intlcur)
return;
try {
await this._intlcur.close();
this._intlcur = undefined;
debug('close');
this.emit('close');
}
catch (err) {
debug('close-error:', err);
this.emit('error', err);
throw err;
}
}
/**
* If cache is enabled, this call fetches and keeps all records in the internal cache.
* Otherwise it throws error. Once all all records fetched,
* you can close Cursor safely and can continue to use it in memory.
* Returns number of fetched rows
*/
async fetchAll() {
if (!this._cache)
throw new Error('fetchAll() method needs cache to be enabled');
const n = this.rowNum;
const v = await this._seek(Number.MAX_SAFE_INTEGER, true);
await this._seek(n - this.rowNum, true);
return v;
}
/**
* Moves cursor to given row number.
* cursor can move both forward and backward if cache enabled.
* Otherwise it throws error.
*/
async moveTo(rowNum) {
await this._seek(rowNum - this.rowNum);
return this.row;
}
/**
* Moves cursor forward by one row and returns that row.
* And also it allows iterating over rows easily.
*/
async next() {
debug('next');
await this._seek(1);
return this.row;
}
/**
* Moves cursor back by one row and returns that row.
* And also it allows iterating over rows easily.
*/
async prev() {
await this._seek(-1);
return this.row;
}
/**
* Moves cursor before first row. (Required cache enabled)
*/
reset() {
if (!this._cache)
throw new Error('reset() method needs cache to be enabled');
this._cache.reset();
this._rowNum = 0;
this.emit('reset');
}
/**
* Moves cursor by given step. If caching is enabled,
* cursor can move both forward and backward. Otherwise it throws error.
*/
async seek(step) {
await this._seek(step);
return this.row;
}
/**
* Creates and returns a readable stream.
*/
toStream(options) {
return new CursorStream(this, options);
}
toString() {
return '[object ' + Object.getPrototypeOf(this).constructor.name + ']';
}
inspect() {
return this.toString();
}
/**
*
*/
async _seek(step, silent) {
step = coerceToInt(step, 0);
if (!step || (step > 0 && this.isClosed))
return this.rowNum;
if (step < 0 && !this._cache)
throw new Error('To move cursor back, it needs cache to be enabled');
const _this = this;
await this._taskQueue
.enqueue(async () => {
/* If moving backward */
if (step < 0) {
/* Seek cache */
while (step < 0 && _this._cache?.cursor) {
_this._row = _this._cache.prev();
_this._rowNum--;
step++;
}
return;
}
while (step > 0) {
if (_this.isEof)
return;
/* Seek cache */
while (step > 0 &&
_this._cache &&
(_this._row = _this._cache.next())) {
_this._rowNum++;
step--;
}
/* Fetch from prefetch cache */
while (step > 0 && (_this._row = _this._fetchCache.shift())) {
_this._rowNum++;
step--;
}
if (_this._fetchedAll) {
_this._rowNum++;
_this.emit('eof');
return;
}
if (!step || _this._fetchedAll)
return;
await _this._fetchRows();
}
})
.toPromise();
if (!silent)
this.emit('move', this.row, this._rowNum);
return this._rowNum;
}
/**
*
*/
async _fetchRows() {
if (!this._intlcur)
throw new Error('Cursor is closed');
let rows = await this._intlcur.fetch(this._prefetchRows);
if (rows && rows.length) {
debug('Fetched %d rows from database', rows.length);
// Normalize rows
rows = this._request.objectRows
? normalizeRowsToObjectRows(this._fields, this._intlcur.rowType, rows, this._request)
: normalizeRowsToArrayRows(this._fields, this._intlcur.rowType, rows, this._request);
callFetchHooks(rows, this._request);
for (const [idx, row] of rows.entries()) {
this.emit('fetch', row, this._rowNum + idx + 1);
}
/* Add rows to cache */
if (this._cache) {
this._cache.push(...rows);
}
else
this._fetchCache.push(...rows);
this._fetchedRows += rows.length;
return;
}
this._fetchedAll = true;
return this.close();
}
}