@google-cloud/bigtable
Version:
Cloud Bigtable Client Library for Node.js
409 lines • 14.3 kB
JavaScript
"use strict";
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChunkTransformer = exports.RowStateEnum = exports.DataEvent = void 0;
const stream_1 = require("stream");
const mutation_1 = require("./mutation");
const table_1 = require("./utils/table");
var DataEvent;
(function (DataEvent) {
DataEvent[DataEvent["LAST_ROW_KEY_UPDATE"] = 0] = "LAST_ROW_KEY_UPDATE";
})(DataEvent || (exports.DataEvent = DataEvent = {}));
class TransformError extends Error {
constructor(props) {
super();
this.name = 'TransformError';
this.message = `${props.message}: ${JSON.stringify(props.chunk)}`;
}
}
/**
* Enum for chunk formatter Row state.
* NEW_ROW: initial state or state after commitRow or resetRow
* ROW_IN_PROGRESS: state after first valid chunk without commitRow or resetRow
* CELL_IN_PROGRESS: state when valueSize > 0(partial cell)
*/
var RowStateEnum;
(function (RowStateEnum) {
RowStateEnum[RowStateEnum["NEW_ROW"] = 1] = "NEW_ROW";
RowStateEnum[RowStateEnum["ROW_IN_PROGRESS"] = 2] = "ROW_IN_PROGRESS";
RowStateEnum[RowStateEnum["CELL_IN_PROGRESS"] = 3] = "CELL_IN_PROGRESS";
})(RowStateEnum || (exports.RowStateEnum = RowStateEnum = {}));
/**
* ChunkTransformer formats all incoming chunks in to row
* keeps all intermediate state until end of stream.
* Should use new instance for each request.
*/
class ChunkTransformer extends stream_1.Transform {
options;
_destroyed;
lastRowKey;
state;
row;
family;
qualifiers;
qualifier;
constructor(options = {}) {
options.objectMode = true; // forcing object mode
super(options);
this.options = options;
this._destroyed = false;
this.lastRowKey = undefined;
this.reset();
}
/**
* called at end of the stream.
* @public
* @param {callback} cb callback will be called with error if there is any uncommitted row
*/
_flush(cb) {
if (typeof this.row.key !== 'undefined') {
this.destroy(new TransformError({
message: 'Response ended with pending row without commit',
chunk: null,
}));
return;
}
cb();
}
/**
* transform the readrowsresponse chunks into friendly format. Chunks contain
* 3 properties:
*
* `rowContents` The row contents, this essentially is all data pertaining
* to a single family.
*
* `commitRow` This is a boolean telling us the all previous chunks for this
* row are ok to consume.
*
* `resetRow` This is a boolean telling us that all the previous chunks are to
* be discarded.
*
* @public
*
* @param {object} data readrows response containing array of chunks.
* @param {object} [_encoding] encoding options.
* @param {callback} next callback will be called once data is processed, with error if any error in processing
*/
_transform(data, _encoding, next) {
for (const chunk of data.chunks) {
switch (this.state) {
case RowStateEnum.NEW_ROW:
this.processNewRow(chunk);
break;
case RowStateEnum.ROW_IN_PROGRESS:
this.processRowInProgress(chunk);
break;
case RowStateEnum.CELL_IN_PROGRESS:
this.processCellInProgress(chunk);
break;
default:
break;
}
if (this._destroyed) {
next();
return;
}
}
if (data.lastScannedRowKey && data.lastScannedRowKey.length > 0) {
this.lastRowKey = mutation_1.Mutation.convertFromBytes(data.lastScannedRowKey, {
userOptions: this.options,
});
/**
* Push an event that will update the lastRowKey in the user stream after
* all rows ahead of this event have reached the user stream. This will
* ensure that a retry excludes the lastScannedRow as this is required
* for the TestReadRows_Retry_LastScannedRow conformance test to pass. It
* is important to use a 'data' event to update the last row key in order
* to allow all the data queued ahead of this event to reach the user
* stream first.
*/
this.push({
eventType: DataEvent.LAST_ROW_KEY_UPDATE,
lastScannedRowKey: this.lastRowKey,
});
}
next();
}
/**
* called when stream is destroyed.
* @public
* @param {error} err error if any
*/
destroy(err) {
if (this._destroyed)
return this;
this._destroyed = true;
if (err) {
this.emit('error', err);
}
this.emit('close');
return this;
}
/**
* Resets state of formatter
* @private
*/
reset() {
this.family = {};
this.qualifiers = [];
this.qualifier = {};
this.row = {};
this.state = RowStateEnum.NEW_ROW;
}
/**
* sets lastRowkey and calls reset when row is committed.
* @private
*/
commit() {
const row = this.row;
this.reset();
this.lastRowKey = row.key;
}
/**
* Validates valuesize and commitrow in a chunk
* @private
* @param {chunk} chunk chunk to validate for valuesize and commitRow
*/
validateValueSizeAndCommitRow(chunk) {
if (chunk.valueSize > 0 && chunk.commitRow) {
this.destroy(new TransformError({
message: 'A row cannot be have a value size and be a commit row',
chunk,
}));
}
}
/**
* Validates resetRow condition for chunk
* @private
* @param {chunk} chunk chunk to validate for resetrow
*/
validateResetRow(chunk) {
const containsData = (chunk.rowKey && chunk.rowKey.length !== 0) ||
chunk.familyName ||
chunk.qualifier ||
(chunk.value && chunk.value.length !== 0) ||
// timestampMicros is an int64 in the protobuf definition,
// which can be either a number or an instance of Long.
// If it's a number...
(typeof chunk.timestampMicros === 'number' &&
chunk.timestampMicros > 0) ||
// If it's an instance of Long...
(typeof chunk.timestampMicros === 'object' &&
'compare' in chunk.timestampMicros &&
typeof chunk.timestampMicros.compare === 'function' &&
chunk.timestampMicros.compare(0) === 1);
if (chunk.resetRow && containsData) {
this.destroy(new TransformError({
message: 'A reset should have no data',
chunk,
}));
}
}
/**
* Validates state for new row.
* @private
* @param {chunk} chunk chunk to validate
* @param {newRowKey} newRowKey newRowKey of the new row
*/
validateNewRow(chunk, newRowKey) {
const row = this.row;
const lastRowKey = this.lastRowKey;
let errorMessage;
if (typeof row.key !== 'undefined') {
errorMessage = 'A new row cannot have existing state';
}
else if (typeof chunk.rowKey === 'undefined' ||
chunk.rowKey.length === 0 ||
newRowKey.length === 0) {
errorMessage = 'A row key must be set';
}
else if (chunk.resetRow) {
errorMessage = 'A new row cannot be reset';
}
else if (lastRowKey === newRowKey) {
errorMessage = 'A commit happened but the same key followed';
}
else if (typeof lastRowKey !== 'undefined' &&
table_1.TableUtils.lessThanOrEqualTo(newRowKey, lastRowKey)) {
errorMessage = 'A row key must be strictly increasing';
}
else if (!chunk.familyName) {
errorMessage = 'A family must be set';
}
else if (chunk.qualifier === null || chunk.qualifier === undefined) {
errorMessage = 'A column qualifier must be set';
}
if (errorMessage) {
this.destroy(new TransformError({ message: errorMessage, chunk }));
return;
}
this.validateValueSizeAndCommitRow(chunk);
}
/**
* Validates state for rowInProgress
* @private
* @param {chunk} chunk chunk to validate
*/
validateRowInProgress(chunk) {
const row = this.row;
if (chunk.rowKey && chunk.rowKey.length) {
const newRowKey = mutation_1.Mutation.convertFromBytes(chunk.rowKey, {
userOptions: this.options,
});
const oldRowKey = row.key || '';
if (newRowKey &&
chunk.rowKey &&
newRowKey.length !== 0 &&
newRowKey.toString() !== oldRowKey.toString()) {
this.destroy(new TransformError({
message: 'A commit is required between row keys',
chunk,
}));
return;
}
}
if (chunk.familyName &&
(chunk.qualifier === null || chunk.qualifier === undefined)) {
this.destroy(new TransformError({
message: 'A qualifier must be specified',
chunk,
}));
return;
}
this.validateResetRow(chunk);
this.validateValueSizeAndCommitRow(chunk);
}
/**
* Validates chunk for cellInProgress state.
* @private
* @param {chunk} chunk chunk to validate
*/
validateCellInProgress(chunk) {
this.validateResetRow(chunk);
this.validateValueSizeAndCommitRow(chunk);
}
/**
* Moves to next state in processing.
* @private
* @param {chunk} chunk chunk in process
*/
moveToNextState(chunk) {
const row = this.row;
if (chunk.commitRow) {
this.push(row);
this.commit();
this.lastRowKey = row.key;
}
else {
if (chunk.valueSize > 0) {
this.state = RowStateEnum.CELL_IN_PROGRESS;
}
else {
this.state = RowStateEnum.ROW_IN_PROGRESS;
}
}
}
/**
* Process chunk when in NEW_ROW state.
* @private
* @param {chunks} chunk chunk to process
*/
processNewRow(chunk) {
const newRowKey = mutation_1.Mutation.convertFromBytes(chunk.rowKey, {
userOptions: this.options,
});
this.validateNewRow(chunk, newRowKey);
if (chunk.familyName && chunk.qualifier) {
const row = this.row;
row.key = newRowKey;
row.data = {};
this.family = row.data[chunk.familyName.value] = {};
const qualifierName = mutation_1.Mutation.convertFromBytes(chunk.qualifier.value, {
userOptions: this.options,
});
this.qualifiers = this.family[qualifierName] = [];
this.qualifier = {
value: mutation_1.Mutation.convertFromBytes(chunk.value, {
userOptions: this.options,
isPossibleNumber: true,
}),
labels: chunk.labels,
timestamp: chunk.timestampMicros,
};
this.qualifiers.push(this.qualifier);
this.moveToNextState(chunk);
}
}
/**
* Process chunk when in ROW_IN_PROGRESS state.
* @private
* @param {chunk} chunk chunk to process
*/
processRowInProgress(chunk) {
this.validateRowInProgress(chunk);
if (chunk.resetRow) {
return this.reset();
}
const row = this.row;
if (chunk.familyName) {
this.family = row.data[chunk.familyName.value] =
row.data[chunk.familyName.value] || {};
}
if (chunk.qualifier) {
const qualifierName = mutation_1.Mutation.convertFromBytes(chunk.qualifier.value, {
userOptions: this.options,
});
this.qualifiers = this.family[qualifierName] =
this.family[qualifierName] || [];
}
this.qualifier = {
value: mutation_1.Mutation.convertFromBytes(chunk.value, {
userOptions: this.options,
isPossibleNumber: true,
}),
labels: chunk.labels,
timestamp: chunk.timestampMicros,
};
this.qualifiers.push(this.qualifier);
this.moveToNextState(chunk);
}
/**
* Process chunk when in CELl_IN_PROGRESS state.
* @private
* @param {chunk} chunk chunk to process
*/
processCellInProgress(chunk) {
this.validateCellInProgress(chunk);
if (chunk.resetRow) {
return this.reset();
}
const chunkQualifierValue = mutation_1.Mutation.convertFromBytes(chunk.value, {
userOptions: this.options,
});
if (chunkQualifierValue instanceof Buffer &&
this.qualifier.value instanceof Buffer) {
this.qualifier.value = Buffer.concat([
this.qualifier.value,
chunkQualifierValue,
]);
}
else {
this.qualifier.value += chunkQualifierValue;
}
this.moveToNextState(chunk);
}
}
exports.ChunkTransformer = ChunkTransformer;
//# sourceMappingURL=chunktransformer.js.map