blob: 0fdee6f36580d7588d260b30b0cfe4862fecb5bb [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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
//
// http://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.
import { createRequire } from 'node:module';
import type { CommandResponse } from '../../client/client.type.js';
import { COMMAND_CODE } from '../command.code.js';
import { responseError } from '../error.utils.js';
import { HEADER_SIZE, encodeRequestHeader } from './header.js';
import { Operation, operationForCode } from './operation.js';
import {
deserializeLoginRegister,
serializeLoginRegister,
serializeLoginRegisterWithPat,
} from './register.js';
import { decodeResponse } from './reply.js';
import { ConsensusSession } from './session.js';
const packageMetadata = createRequire(import.meta.url)(
'../../../package.json'
) as { version: string };
const SDK_VERSION = packageMetadata.version;
const MAX_U32 = 0xFFFF_FFFF;
/** `IggyError::Unauthenticated`, matching the Rust SDK's unbound-session error. */
const UNAUTHENTICATED = 40;
export class VsrSession {
private state: ConsensusSession;
constructor(clientId?: bigint) {
this.state = new ConsensusSession(clientId);
}
reset(): void {
this.state = new ConsensusSession();
}
bind(session: bigint): void {
this.state.bind(session);
}
get hasActivity(): boolean {
return this.state.hasActivity;
}
encode(command: number, payload: Buffer): Buffer {
const operation = registerCommand(command)
? Operation.Register
: operationForCode(command);
const size = HEADER_SIZE + payload.length;
if (size > MAX_U32)
throw new RangeError('VSR request exceeds the u32 frame-size limit');
let request: bigint;
let session: bigint;
if (operation === Operation.Register) {
request = this.state.beginRegister();
session = 0n;
} else if (operation === Operation.NonReplicated) {
request = this.state.currentRequestId();
session = this.state.session ?? 0n;
} else {
if (this.state.session === null)
throw responseError(command, UNAUTHENTICATED);
// Partition ops consume an id too, even though no partition-plane dedup
// exists yet: dedup needs each send to carry a distinct number, and the
// metadata watermark tolerates the gaps.
request = this.state.nextRequestId();
session = this.state.session;
}
const header = encodeRequestHeader({
size,
client: this.state.clientId,
request,
operation,
session,
nonReplicatedCode:
operation === Operation.NonReplicated ? command : undefined,
});
return payload.length === 0 ? header : Buffer.concat([header, payload]);
}
}
export const decodeVsrResponse = (
frame: Buffer,
commandCode = 0
): CommandResponse => {
const data = decodeResponse(frame, commandCode);
return { status: 0, length: data.length, data };
};
export const readRegisteredSession = (response: CommandResponse): bigint =>
deserializeLoginRegister(response.data).session;
export const prepareVsrCommand = (
command: number,
payload: Buffer
): { command: number, payload: Buffer } => {
if (command === COMMAND_CODE.LoginUser) {
const username = readWireName(payload, 0);
const password = readWireName(payload, username.next);
return {
command: COMMAND_CODE.LoginRegister,
payload: serializeLoginRegister(username.value, password.value, SDK_VERSION),
};
}
if (command === COMMAND_CODE.LoginWithAccessToken) {
const token = readWireName(payload, 0);
return {
command: COMMAND_CODE.LoginRegisterWithAccessToken,
payload: serializeLoginRegisterWithPat(token.value, SDK_VERSION),
};
}
return { command, payload };
};
const registerCommand = (command: number): boolean =>
command === COMMAND_CODE.LoginRegister ||
command === COMMAND_CODE.LoginRegisterWithAccessToken;
const readWireName = (
payload: Buffer,
offset: number
): { value: string, next: number } => {
if (payload.length <= offset)
throw new Error('wire name length is missing');
const length = payload.readUInt8(offset);
const next = offset + 1 + length;
if (length === 0 || payload.length < next)
throw new Error('wire name is incomplete');
return { value: payload.subarray(offset + 1, next).toString('utf8'), next };
};
export { HEADER_SIZE } from './header.js';
export { VsrEvictionError } from './reply.js';