blob: e7128a1159584606cb5fcff0e5f4cadb1a7a3001 [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.
/**
* VSR consensus header layout, ported from
* `core/binary_protocol/src/consensus/header.rs`. Every consensus frame in
* both directions starts with a 256-byte header. Fields are read and written
* by byte offset, mirroring the Rust SDK, which avoids struct casts because
* the headers lead with unaligned u128 fields.
*/
/** Size of every consensus header, both directions. */
export const HEADER_SIZE = 256;
/**
* `RequestHeader` field offsets the client writes.
*
* The client wire carries no routing namespace: the server derives the
* consensus group (plane from `operation`, partition target from the payload)
* and stamps it into its own internal header. Everything that followed the
* removed field therefore sits eight bytes earlier than in the pre-derivation
* layout.
*/
export const REQUEST_OFFSET = {
size: 48,
command: 60,
client: 128,
timestamp: 160,
request: 168,
operation: 176,
session: 184,
reserved: 196
} as const;
/** `ReplyHeader` field offsets the client reads. */
export const REPLY_OFFSET = {
size: 48,
command: 60,
operation: 208,
status: 216
} as const;
/** `EvictionHeader` field offsets the client reads. */
export const EVICTION_OFFSET = {
command: 60,
client: 128,
serverProtocolVersion: 144,
serverProtocolVersionMin: 148,
reason: 255
} as const;
/** `Command` frame discriminants a client encounters. */
export const Command = {
Request: 5,
Reply: 8,
Eviction: 13
} as const;
/**
* `EvictionReason` values, mirroring
* `core/binary_protocol/src/consensus/header.rs`.
*/
export const EvictionReason = {
Reserved: 0,
NoSession: 1,
ClientReleaseTooLow: 2,
ClientReleaseTooHigh: 3,
InvalidRequestOperation: 4,
InvalidRequestBody: 5,
InvalidRequestBodySize: 6,
SessionTooLow: 7,
SessionReleaseMismatch: 8,
InvalidCredentials: 9,
InvalidToken: 10,
UserInactive: 11,
SessionError: 12,
StaleClient: 13,
IncompatibleProtocol: 14,
MalformedLogin: 15
} as const;
/** Fields the client writes into a request header. */
export type RequestHeaderFields = {
/** Header + body total, bytes. */
size: number,
/** Ephemeral client identifier (u128). */
client: bigint,
/** Request id (u64): 0 for Register, else per session rules. */
request: bigint,
/** `Operation` discriminant. */
operation: number,
/** Bound session (u64), or 0n. */
session: bigint,
/** Command code for `NonReplicated`, placed in `reserved[0..4]`. */
nonReplicatedCode?: number
};
const U64_MASK = 0xFFFFFFFFFFFFFFFFn;
/**
* Encodes a 256-byte request header. Only the six fields the server reads
* are written. The checksums stay zero: the frame and body checksums are not
* read on the client request path, and `request_checksum` treats zero as
* unstamped, which opts out of the server's payload comparison. Stamping it is
* optional -- the Rust SDK does for deduped ops, this SDK does not yet.
*/
export const encodeRequestHeader = (fields: RequestHeaderFields): Buffer => {
const header = Buffer.alloc(HEADER_SIZE);
header.writeUInt32LE(fields.size, REQUEST_OFFSET.size);
header.writeUInt8(Command.Request, REQUEST_OFFSET.command);
// u128 client id: two little-endian u64 halves, low half first.
header.writeBigUInt64LE(fields.client & U64_MASK, REQUEST_OFFSET.client);
header.writeBigUInt64LE(fields.client >> 64n, REQUEST_OFFSET.client + 8);
header.writeBigUInt64LE(fields.request, REQUEST_OFFSET.request);
header.writeUInt8(fields.operation, REQUEST_OFFSET.operation);
header.writeBigUInt64LE(fields.session, REQUEST_OFFSET.session);
if (fields.nonReplicatedCode !== undefined)
header.writeUInt32LE(fields.nonReplicatedCode, REQUEST_OFFSET.reserved);
return header;
};
/**
* Reads the frame discriminant. `command` sits at the same offset in every
* consensus header, so one byte discriminates the frame type.
*/
export const peekCommand = (header: Buffer): number =>
header.readUInt8(REPLY_OFFSET.command);
/** Reads the total frame size from a reply or eviction header. */
export const readSize = (header: Buffer): number =>
header.readUInt32LE(REPLY_OFFSET.size);
/** Reads the pre-commit denial status; 0 means the reply committed. */
export const readStatus = (header: Buffer): number =>
header.readUInt32LE(REPLY_OFFSET.status);
/** Reads the `Operation` discriminant from a reply header. */
export const readReplyOperation = (header: Buffer): number =>
header.readUInt8(REPLY_OFFSET.operation);
/** Decoded eviction frame. */
export type Eviction = {
reason: number,
serverProtocolVersion: number,
serverProtocolVersionMin: number
};
/** Reads the fields of an eviction header the client acts on. */
export const readEviction = (header: Buffer): Eviction => ({
reason: header.readUInt8(EVICTION_OFFSET.reason),
serverProtocolVersion:
header.readUInt32LE(EVICTION_OFFSET.serverProtocolVersion),
serverProtocolVersionMin:
header.readUInt32LE(EVICTION_OFFSET.serverProtocolVersionMin)
});