From 09b31d6694e392351671117d44fff8fc3fe72a0a Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 10 Mar 2026 16:51:04 +0530 Subject: [PATCH 1/3] feat: base types updated for draft-16 --- lib/transport/base_data.ts | 430 +++++++++++++----- lib/transport/connection.ts | 13 +- lib/transport/control/client_setup.ts | 53 +-- lib/transport/control/fetch.ts | 253 +++++------ lib/transport/control/fetch_error.ts | 36 -- lib/transport/control/fetch_ok.ts | 88 ++-- lib/transport/control/index.ts | 321 ++++++------- lib/transport/control/max_request_id.ts | 12 +- lib/transport/control/namespace.ts | 27 ++ lib/transport/control/namespace_done.ts | 27 ++ lib/transport/control/publish.ts | 106 ++--- lib/transport/control/publish_done.ts | 59 +-- lib/transport/control/publish_error.ts | 35 -- lib/transport/control/publish_namespace.ts | 57 +-- .../control/publish_namespace_cancel.ts | 35 ++ .../control/publish_namespace_done.ts | 36 +- .../control/publish_namespace_error.ts | 36 -- lib/transport/control/publish_namespace_ok.ts | 26 -- lib/transport/control/publish_ok.ts | 90 ++-- lib/transport/control/request_error.ts | 41 ++ lib/transport/control/request_ok.ts | 36 ++ lib/transport/control/requests_block.ts | 27 -- lib/transport/control/requests_blocked.ts | 30 ++ lib/transport/control/server_setup.ts | 41 +- lib/transport/control/subscribe.ts | 197 ++++---- lib/transport/control/subscribe_error.ts | 35 -- lib/transport/control/subscribe_namespace.ts | 70 +-- .../control/subscribe_namespace_error.ts | 36 -- .../control/subscribe_namespace_ok.ts | 28 -- lib/transport/control/subscribe_ok.ts | 102 ++--- lib/transport/control/subscribe_update.ts | 75 ++- lib/transport/control/track_status.ts | 42 ++ lib/transport/objects.ts | 158 ++++--- lib/transport/stream.ts | 111 ++--- lib/transport/subgroup.ts | 122 +++-- 35 files changed, 1522 insertions(+), 1369 deletions(-) delete mode 100644 lib/transport/control/fetch_error.ts create mode 100644 lib/transport/control/namespace.ts create mode 100644 lib/transport/control/namespace_done.ts delete mode 100644 lib/transport/control/publish_error.ts create mode 100644 lib/transport/control/publish_namespace_cancel.ts delete mode 100644 lib/transport/control/publish_namespace_error.ts delete mode 100644 lib/transport/control/publish_namespace_ok.ts create mode 100644 lib/transport/control/request_error.ts create mode 100644 lib/transport/control/request_ok.ts delete mode 100644 lib/transport/control/requests_block.ts create mode 100644 lib/transport/control/requests_blocked.ts delete mode 100644 lib/transport/control/subscribe_error.ts delete mode 100644 lib/transport/control/subscribe_namespace_error.ts delete mode 100644 lib/transport/control/subscribe_namespace_ok.ts create mode 100644 lib/transport/control/track_status.ts diff --git a/lib/transport/base_data.ts b/lib/transport/base_data.ts index dc90d56..43ff993 100644 --- a/lib/transport/base_data.ts +++ b/lib/transport/base_data.ts @@ -4,140 +4,338 @@ export type Tuple = TupleField[] export type TupleField = T // can be any type export namespace Tuple { - // Serialize implementation - only works for types where TupleField.serialize is implemented - export function serialize(tuple: Tuple): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(tuple.length) - tuple.forEach(field => { - const serialized = TupleField.serialize(field) - buf.putBytes(serialized) - }) - return buf.Uint8Array - } - - // Deserialize implementation - only works for types where TupleField.deserialize is implemented - export function deserialize(buffer: ImmutableBytesBuffer): Tuple { - const tuple: T[] = [] - const len = buffer.getVarInt() - for (let i = 0; i < len; i++) { - const field = TupleField.deserialize(buffer) - tuple.push(field) - } - return tuple - } + // Serialize implementation - only works for types where TupleField.serialize is implemented + export function serialize(tuple: Tuple): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(tuple.length) + tuple.forEach(field => { + const serialized = TupleField.serialize(field) + buf.putBytes(serialized) + }) + return buf.Uint8Array + } + + // Deserialize implementation - only works for types where TupleField.deserialize is implemented + export function deserialize(buffer: ImmutableBytesBuffer): Tuple { + const tuple: T[] = [] + const len = buffer.getVarInt() + for (let i = 0; i < len; i++) { + const field = TupleField.deserialize(buffer) + tuple.push(field) + } + return tuple + } } export namespace TupleField { - // Serialize implementation for string fields only - export function serialize(field: T): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - const encoded = new TextEncoder().encode(field) - buf.putVarInt(encoded.length) - buf.putBytes(encoded) - return buf.Uint8Array - } - - // Deserialize implementation for string fields only - export function deserialize(buffer: ImmutableBytesBuffer): T { - const field = buffer.getVarBytes() - return new TextDecoder().decode(field) as T - } + // Serialize implementation for string fields only + export function serialize(field: T): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + const encoded = new TextEncoder().encode(field) + buf.putVarInt(encoded.length) + buf.putBytes(encoded) + return buf.Uint8Array + } + + // Deserialize implementation for string fields only + export function deserialize(buffer: ImmutableBytesBuffer): T { + const field = buffer.getVarBytes() + return new TextDecoder().decode(field) as T + } } export type Location = { - group: bigint - object: bigint + group: bigint + object: bigint } export namespace Location { - export function serialize(location: Location): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(location.group) - buf.putVarInt(location.object) - return buf.Uint8Array - } - - export function deserialize(buffer: ImmutableBytesBuffer): Location { - const group = buffer.getVarInt() - const object = buffer.getVarInt() - return { group, object } - } + export function serialize(location: Location): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(location.group) + buf.putVarInt(location.object) + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): Location { + const group = buffer.getVarInt() + const object = buffer.getVarInt() + return { group, object } + } } -// TODO(itzmanish): have checks for key type for even or odd +// Draft-16: Key-Value-Pairs use delta-encoded types (Section 1.4.2) +// Delta Type is delta from previous type (or 0 if first) +// Type even => value is varint (no length prefix) +// Type odd => value is length-prefixed bytes export type KeyValuePairs = Map export type Parameters = KeyValuePairs export namespace KeyValuePairs { - export function valueIsVarInt(key: bigint): boolean { - return (key & 1n) === 0n - } - - export function serialize(pairs: Parameters): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(pairs.size) - pairs.forEach((value, key) => { - buf.putVarInt(key) - if (valueIsVarInt(key)) { - buf.putVarInt(value as bigint) - } else { - buf.putBytes(value as Uint8Array) - } - }) - - return buf.Uint8Array - } - - export function deserialize(buffer: ImmutableBytesBuffer): KeyValuePairs { - const size = buffer.getNumberVarInt() - return deserialize_with_size(buffer, size) - } - - export function deserialize_with_size(buffer: ImmutableBytesBuffer, size: number): KeyValuePairs { - const pairs = new Map() - for (let i = 0; i < size; i++) { - const key = buffer.getVarInt() - const value = valueIsVarInt(key) ? buffer.getVarInt() : buffer.getVarBytes() - pairs.set(key, value) - } - - return pairs - } - - export async function deserialize_with_reader(reader: Reader): Promise { - const size = await reader.getNumberVarInt() - const pairs = new Map() - for (let i = 0; i < size; i++) { - const key = await reader.getVarInt() - const value = valueIsVarInt(key) ? await reader.getVarInt() : await reader.getVarBytes() - pairs.set(key, value) - } - - return pairs - } + export function valueIsVarInt(key: bigint): boolean { + return (key & 1n) === 0n + } + + export function serialize(pairs: KeyValuePairs): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + let prevType = 0n + // Sort keys to ensure ascending order for delta encoding + const sortedEntries = [...pairs.entries()].sort((a, b) => { + if (a[0] < b[0]) return -1 + if (a[0] > b[0]) return 1 + return 0 + }) + for (const [key, value] of sortedEntries) { + const delta = key - prevType + buf.putVarInt(delta) + if (valueIsVarInt(key)) { + buf.putVarInt(value as bigint) + } else { + const bytes = value as Uint8Array + buf.putVarInt(bytes.length) + buf.putBytes(bytes) + } + prevType = key + } + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): KeyValuePairs { + const pairs = new Map() + let prevType = 0n + while (buffer.remaining > 0) { + const delta = buffer.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = buffer.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = buffer.getNumberVarInt() + const value = buffer.getBytes(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } + + export function deserialize_with_count(buffer: ImmutableBytesBuffer, count: number): KeyValuePairs { + const pairs = new Map() + let prevType = 0n + for (let i = 0; i < count; i++) { + const delta = buffer.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = buffer.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = buffer.getNumberVarInt() + const value = buffer.getBytes(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } + + export async function deserialize_with_reader(reader: Reader): Promise { + const pairs = new Map() + let prevType = 0n + while (!(await reader.done())) { + const delta = await reader.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = await reader.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = await reader.getNumberVarInt() + const value = await reader.read(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } + + export async function deserialize_with_reader_count(reader: Reader, count: number): Promise { + const pairs = new Map() + let prevType = 0n + for (let i = 0; i < count; i++) { + const delta = await reader.getVarInt() + const resolvedType = prevType + delta + if (valueIsVarInt(resolvedType)) { + const value = await reader.getVarInt() + pairs.set(resolvedType, value) + } else { + const length = await reader.getNumberVarInt() + const value = await reader.read(length) + pairs.set(resolvedType, value) + } + prevType = resolvedType + } + return pairs + } } export namespace Parameters { - export function valueIsVarInt(key: bigint): boolean { - return KeyValuePairs.valueIsVarInt(key) - } - - export function serialize(pairs: Parameters): Uint8Array { - return KeyValuePairs.serialize(pairs) - } - - export function deserialize(buffer: ImmutableBytesBuffer): Parameters { - return KeyValuePairs.deserialize(buffer) - } - - export function deserialize_with_size(buffer: ImmutableBytesBuffer, size: number): Parameters { - return KeyValuePairs.deserialize_with_size(buffer, size) - } - - export async function deserialize_with_reader(reader: Reader): Promise { - return KeyValuePairs.deserialize_with_reader(reader) - } -} \ No newline at end of file + export function valueIsVarInt(key: bigint): boolean { + return KeyValuePairs.valueIsVarInt(key) + } + + export function serialize(pairs: Parameters): Uint8Array { + return KeyValuePairs.serialize(pairs) + } + + export function deserialize(buffer: ImmutableBytesBuffer): Parameters { + return KeyValuePairs.deserialize(buffer) + } + + export function deserialize_with_count(buffer: ImmutableBytesBuffer, count: number): Parameters { + return KeyValuePairs.deserialize_with_count(buffer, count) + } + + export async function deserialize_with_reader(reader: Reader): Promise { + return KeyValuePairs.deserialize_with_reader(reader) + } + + export async function deserialize_with_reader_count(reader: Reader, count: number): Promise { + return KeyValuePairs.deserialize_with_reader_count(reader, count) + } +} + + +// Draft-16: Reason Phrase structure (Section 1.4.3) +// Max length is 1024 bytes +export const REASON_PHRASE_MAX_LENGTH = 1024 + +export type ReasonPhrase = string + +export namespace ReasonPhrase { + export function serialize(reason: ReasonPhrase): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + const encoded = new TextEncoder().encode(reason) + if (encoded.length > REASON_PHRASE_MAX_LENGTH) { + throw new Error(`Reason phrase exceeds max length of ${REASON_PHRASE_MAX_LENGTH} bytes`) + } + buf.putVarInt(encoded.length) + buf.putBytes(encoded) + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): ReasonPhrase { + const length = buffer.getNumberVarInt() + if (length > REASON_PHRASE_MAX_LENGTH) { + throw new Error(`Reason phrase length ${length} exceeds max of ${REASON_PHRASE_MAX_LENGTH}`) + } + const bytes = buffer.getBytes(length) + return new TextDecoder().decode(bytes) + } + + export async function deserialize_with_reader(reader: Reader): Promise { + const length = await reader.getNumberVarInt() + if (length > REASON_PHRASE_MAX_LENGTH) { + throw new Error(`Reason phrase length ${length} exceeds max of ${REASON_PHRASE_MAX_LENGTH}`) + } + const bytes = await reader.read(length) + return new TextDecoder().decode(bytes) + } +} + + +// Draft-16: Parameter Type constants (Section 13.2, Table 8) +export enum ParameterType { + DELIVERY_TIMEOUT = 0x02, + AUTHORIZATION_TOKEN = 0x03, + EXPIRES = 0x08, + LARGEST_OBJECT = 0x09, + FORWARD = 0x10, + SUBSCRIBER_PRIORITY = 0x20, + SUBSCRIPTION_FILTER = 0x21, + GROUP_ORDER = 0x22, + NEW_GROUP_REQUEST = 0x32, +} + + +// Draft-16: Extension Header Type constants (Section 13.3, Table 9) +export enum ExtensionHeaderType { + DELIVERY_TIMEOUT = 0x02, + MAX_CACHE_DURATION = 0x04, + IMMUTABLE_EXTENSIONS = 0x0B, + DEFAULT_PUBLISHER_PRIORITY = 0x0E, + DEFAULT_PUBLISHER_GROUP_ORDER = 0x22, + DYNAMIC_GROUPS = 0x30, + PRIOR_GROUP_ID_GAP = 0x3C, + PRIOR_OBJECT_ID_GAP = 0x3E, +} + + +// Draft-16: Session Termination Error Codes (Section 13.4.1, Table 10) +export enum SessionTerminationError { + NO_ERROR = 0x0, + INTERNAL_ERROR = 0x1, + UNAUTHORIZED = 0x2, + PROTOCOL_VIOLATION = 0x3, + INVALID_REQUEST_ID = 0x4, + DUPLICATE_TRACK_ALIAS = 0x5, + KEY_VALUE_FORMATTING_ERROR = 0x6, + TOO_MANY_REQUESTS = 0x7, + INVALID_PATH = 0x8, + MALFORMED_PATH = 0x9, + GOAWAY_TIMEOUT = 0x10, + CONTROL_MESSAGE_TIMEOUT = 0x11, + DATA_STREAM_TIMEOUT = 0x12, + AUTH_TOKEN_CACHE_OVERFLOW = 0x13, + DUPLICATE_AUTH_TOKEN_ALIAS = 0x14, + VERSION_NEGOTIATION_FAILED = 0x15, + MALFORMED_AUTH_TOKEN = 0x16, + UNKNOWN_AUTH_TOKEN_ALIAS = 0x17, + EXPIRED_AUTH_TOKEN = 0x18, + INVALID_AUTHORITY = 0x19, + MALFORMED_AUTHORITY = 0x1A, +} + + +// Draft-16: REQUEST_ERROR Codes (Section 13.4.2, Table 11) +export enum RequestErrorCode { + INTERNAL_ERROR = 0x0, + UNAUTHORIZED = 0x1, + TIMEOUT = 0x2, + NOT_SUPPORTED = 0x3, + MALFORMED_AUTH_TOKEN = 0x4, + EXPIRED_AUTH_TOKEN = 0x5, + DOES_NOT_EXIST = 0x10, + INVALID_RANGE = 0x11, + MALFORMED_TRACK = 0x12, + DUPLICATE_SUBSCRIPTION = 0x19, + UNINTERESTED = 0x20, + PREFIX_OVERLAP = 0x30, + INVALID_JOINING_REQUEST_ID = 0x32, +} + + +// Draft-16: PUBLISH_DONE Codes (Section 13.4.3, Table 12) +export enum PublishDoneCode { + INTERNAL_ERROR = 0x0, + UNAUTHORIZED = 0x1, + TRACK_ENDED = 0x2, + SUBSCRIPTION_ENDED = 0x3, + GOING_AWAY = 0x4, + EXPIRED = 0x5, + TOO_FAR_BEHIND = 0x6, + UPDATE_FAILED = 0x8, + MALFORMED_TRACK = 0x12, +} + + +// Draft-16: Data Stream Reset Error Codes (Section 13.4.4, Table 13) +export enum DataStreamResetCode { + INTERNAL_ERROR = 0x0, + CANCELLED = 0x1, + DELIVERY_TIMEOUT = 0x2, + SESSION_CLOSED = 0x3, + UNKNOWN_OBJECT_STATUS = 0x4, + MALFORMED_TRACK = 0x12, +} diff --git a/lib/transport/connection.ts b/lib/transport/connection.ts index 2c0f74f..418f376 100644 --- a/lib/transport/connection.ts +++ b/lib/transport/connection.ts @@ -95,7 +95,18 @@ export class Connection { } async #recv(msg: Control.MessageWithType) { - if (Control.isPublisher(msg.type)) { + // RequestOk and RequestError can be sent by either side, + // so route based on request ID parity (even=subscriber-initiated, odd=publisher-initiated) + if (msg.type === Control.ControlMessageType.RequestOk || msg.type === Control.ControlMessageType.RequestError) { + const id = (msg.message as { id: bigint }).id + if (id % 2n === 0n) { + // Even request ID = subscriber-initiated request, response goes to subscriber + await this.#subscriber.recv(msg) + } else { + // Odd request ID = publisher-initiated request, response goes to publisher + await this.#publisher.recv(msg) + } + } else if (Control.isPublisher(msg.type)) { await this.#subscriber.recv(msg) } else { await this.#publisher.recv(msg) diff --git a/lib/transport/control/client_setup.ts b/lib/transport/control/client_setup.ts index 24c190c..a1ac3f8 100644 --- a/lib/transport/control/client_setup.ts +++ b/lib/transport/control/client_setup.ts @@ -1,38 +1,31 @@ import { ControlMessageType, Version } from "." -import { KeyValuePairs, Parameters } from "../base_data" +import { Parameters } from "../base_data" import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" export interface ClientSetup { - versions: Version[] - params: Parameters + params: Parameters } export namespace ClientSetup { - export function serialize(v: ClientSetup): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.ClientSetup) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.versions.length) - v.versions.forEach((version) => { - payloadBuf.putVarInt(version) - }) - payloadBuf.putBytes(Parameters.serialize(v.params)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - console.log("client setup: payload len:", payloadBuf.length, "msg len:", mainBuf.length) - return mainBuf.Uint8Array - } + export function serialize(v: ClientSetup): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.ClientSetup) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + // Draft-16: Number of Parameters + delta-encoded parameters only + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) + payloadBuf.putBytes(paramsBytes) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): ClientSetup { - const supportedVersionLen = reader.getNumberVarInt() - const versions: Version[] = [] - for (let i = 0; i < supportedVersionLen; i++) { - versions.push(reader.getNumberVarInt() as Version) - } - const params = Parameters.deserialize(reader) - return { - versions, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): ClientSetup { + // Draft-16: Number of Parameters + parameters + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + params + } + } +} diff --git a/lib/transport/control/fetch.ts b/lib/transport/control/fetch.ts index 2d0e466..088bd63 100644 --- a/lib/transport/control/fetch.ts +++ b/lib/transport/control/fetch.ts @@ -1,158 +1,153 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Location, Tuple, Parameters } from "../base_data" export enum FetchType { - Standalone = 0x1, - Relative = 0x2, - Absolute = 0x3, + Standalone = 0x1, + Relative = 0x2, + Absolute = 0x3, } export namespace FetchType { - export function serialize(v: FetchType): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(v) - return buf.Uint8Array - } - - export function deserialize(buffer: ImmutableBytesBuffer): FetchType { - const order = buffer.getVarInt() - switch (order) { - case 1n: - return FetchType.Standalone - case 2n: - return FetchType.Relative - case 3n: - return FetchType.Absolute - default: - throw new Error(`Invalid FetchType value: ${order}`) - } - } + export function serialize(v: FetchType): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(v) + return buf.Uint8Array + } + + export function deserialize(buffer: ImmutableBytesBuffer): FetchType { + const order = buffer.getVarInt() + switch (order) { + case 1n: + return FetchType.Standalone + case 2n: + return FetchType.Relative + case 3n: + return FetchType.Absolute + default: + throw new Error(`Invalid FetchType value: ${order}`) + } + } } export interface StandaloneFetch { - namespace: Tuple - name: string - start_location: Location - end_location: Location + namespace: Tuple + name: string + start_location: Location + end_location: Location } export namespace StandaloneFetch { - export function serialize(v: StandaloneFetch): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Fetch) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putUtf8String(v.name) - payloadBuf.putBytes(Location.serialize(v.start_location)) - payloadBuf.putBytes(Location.serialize(v.end_location)) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(r: ImmutableBytesBuffer): StandaloneFetch { - const namespace = Tuple.deserialize(r) - const name = r.getUtf8String() - const start_location = Location.deserialize(r) - const end_location = Location.deserialize(r) - return { - namespace, - name, - start_location, - end_location - } - } + export function serialize(v: StandaloneFetch): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Fetch) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + payloadBuf.putBytes(Location.serialize(v.start_location)) + payloadBuf.putBytes(Location.serialize(v.end_location)) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(r: ImmutableBytesBuffer): StandaloneFetch { + const namespace = Tuple.deserialize(r) + const name = r.getUtf8String() + const start_location = Location.deserialize(r) + const end_location = Location.deserialize(r) + return { + namespace, + name, + start_location, + end_location + } + } } export interface JoiningFetch { - id: bigint - start: bigint + id: bigint + start: bigint } export namespace JoiningFetch { - export function serialize(v: JoiningFetch): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Fetch) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.start) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(r: ImmutableBytesBuffer): JoiningFetch { - const id = r.getVarInt() - const start = r.getVarInt() - return { - id, - start - } - } + export function serialize(v: JoiningFetch): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Fetch) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.start) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(r: ImmutableBytesBuffer): JoiningFetch { + const id = r.getVarInt() + const start = r.getVarInt() + return { + id, + start + } + } } export interface Fetch { - id: bigint - subscriber_priority: number - group_order: GroupOrder - fetch_type: FetchType - standalone?: StandaloneFetch - joining?: JoiningFetch - params?: Parameters + id: bigint + fetch_type: FetchType + standalone?: StandaloneFetch + joining?: JoiningFetch + params?: Parameters } export namespace Fetch { - export function serialize(v: Fetch): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Fetch) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putU8(v.subscriber_priority) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putBytes(FetchType.serialize(v.fetch_type)) - if (v.standalone) { - payloadBuf.putBytes(StandaloneFetch.serialize(v.standalone)) - } - if (v.joining) { - payloadBuf.putBytes(JoiningFetch.serialize(v.joining)) - } - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): Fetch { - const id = reader.getVarInt() - const subscriber_priority = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const fetch_type = FetchType.deserialize(reader) - let standalone: StandaloneFetch | undefined - let joining: JoiningFetch | undefined - let params: Parameters | undefined - if (fetch_type == FetchType.Standalone) { - standalone = StandaloneFetch.deserialize(reader) - } else if (fetch_type == FetchType.Relative || fetch_type == FetchType.Absolute) { - joining = JoiningFetch.deserialize(reader) - } - if (reader.remaining > 0) { - params = Parameters.deserialize(reader) - } - return { - id, - subscriber_priority, - group_order, - fetch_type, - standalone, - joining, - params - } - } + export function serialize(v: Fetch): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Fetch) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(FetchType.serialize(v.fetch_type)) + if (v.standalone) { + payloadBuf.putBytes(StandaloneFetch.serialize(v.standalone)) + } + if (v.joining) { + payloadBuf.putBytes(JoiningFetch.serialize(v.joining)) + } + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): Fetch { + const id = reader.getVarInt() + const fetch_type = FetchType.deserialize(reader) + let standalone: StandaloneFetch | undefined + let joining: JoiningFetch | undefined + let params: Parameters | undefined + if (fetch_type == FetchType.Standalone) { + standalone = StandaloneFetch.deserialize(reader) + } else if (fetch_type == FetchType.Relative || fetch_type == FetchType.Absolute) { + joining = JoiningFetch.deserialize(reader) + } + const numParams = reader.getNumberVarInt() + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + return { + id, + fetch_type, + standalone, + joining, + params + } + } } diff --git a/lib/transport/control/fetch_error.ts b/lib/transport/control/fetch_error.ts deleted file mode 100644 index c061514..0000000 --- a/lib/transport/control/fetch_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface FetchError { - id: bigint - code: bigint - reason: string -} - - -export namespace FetchError { - export function serialize(v: FetchError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.FetchError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): FetchError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/fetch_ok.ts b/lib/transport/control/fetch_ok.ts index be2fa31..984fad2 100644 --- a/lib/transport/control/fetch_ok.ts +++ b/lib/transport/control/fetch_ok.ts @@ -1,47 +1,57 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Location } from "../base_data" +import { Parameters, Location, KeyValuePairs } from "../base_data" export interface FetchOk { - id: bigint - group_order: GroupOrder - end_of_track: number // u8 - end_location: Location - params?: Parameters + id: bigint + end_of_track: number // u8 + end_location: Location + track_extensions?: KeyValuePairs + params?: Parameters } - export namespace FetchOk { - export function serialize(v: FetchOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.FetchOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putU8(v.end_of_track) - payloadBuf.putBytes(Location.serialize(v.end_location)) - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) + export function serialize(v: FetchOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.FetchOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putU8(v.end_of_track) + payloadBuf.putBytes(Location.serialize(v.end_location)) + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) + const extensions = v.track_extensions ?? new Map() + const extBytes = KeyValuePairs.serialize(extensions) + payloadBuf.putVarInt(BigInt(extBytes.length)) + payloadBuf.putBytes(extBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): FetchOk { - const id = reader.getVarInt() - const group_order = GroupOrder.deserialize(reader) - const end_of_track = reader.getU8() - const end_location = Location.deserialize(reader) - let params: Parameters | undefined - if (reader.remaining > 0) { - params = Parameters.deserialize(reader) - } - return { - id, - group_order, - end_of_track, - end_location, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): FetchOk { + const id = reader.getVarInt() + const end_of_track = reader.getU8() + const end_location = Location.deserialize(reader) + const numParams = reader.getNumberVarInt() + let params: Parameters | undefined + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + const extLength = reader.getNumberVarInt() + let track_extensions: KeyValuePairs | undefined + if (extLength > 0) { + const extData = reader.getBytes(Number(extLength)) + track_extensions = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extData)) + } + return { + id, + end_of_track, + end_location, + track_extensions, + params + } + } +} diff --git a/lib/transport/control/index.ts b/lib/transport/control/index.ts index fe80f58..e78b666 100644 --- a/lib/transport/control/index.ts +++ b/lib/transport/control/index.ts @@ -1,197 +1,212 @@ import { Subscribe, GroupOrder, FilterType } from "./subscribe" import { SubscribeOk } from "./subscribe_ok" -import { SubscribeError } from "./subscribe_error" import { SubscribeUpdate } from "./subscribe_update" -import { SubscribeNamespace } from "./subscribe_namespace" -import { SubscribeNamespaceOk } from "./subscribe_namespace_ok" -import { SubscribeNamespaceError } from "./subscribe_namespace_error" +import { SubscribeNamespace, SubscribeOptions } from "./subscribe_namespace" import { Unsubscribe } from "./unsubscribe" import { Publish } from "./publish" import { PublishOk } from "./publish_ok" -import { PublishError } from "./publish_error" import { PublishDone } from "./publish_done" import { PublishNamespace } from "./publish_namespace" -import { PublishNamespaceOk } from "./publish_namespace_ok" -import { PublishNamespaceError } from "./publish_namespace_error" import { PublishNamespaceDone } from "./publish_namespace_done" +import { PublishNamespaceCancel } from "./publish_namespace_cancel" +import { Namespace } from "./namespace" +import { NamespaceDone } from "./namespace_done" +import { TrackStatus } from "./track_status" import { Fetch } from "./fetch" import { FetchOk } from "./fetch_ok" -import { FetchError } from "./fetch_error" import { FetchCancel } from "./fetch_cancel" import { GoAway } from "./go_away" import { ClientSetup } from "./client_setup" import { ServerSetup } from "./server_setup" +import { RequestOk } from "./request_ok" +import { RequestError } from "./request_error" +import { MaxRequestId } from "./max_request_id" +import { RequestsBlocked } from "./requests_blocked" enum Version { - DRAFT_00 = 0xff000000, - DRAFT_01 = 0xff000001, - DRAFT_02 = 0xff000002, - DRAFT_03 = 0xff000003, - DRAFT_04 = 0xff000004, - DRAFT_05 = 0xff000005, - DRAFT_06 = 0xff000006, - DRAFT_07 = 0xff000007, - DRAFT_14 = 0xff00000e, - KIXEL_00 = 0xbad00, - KIXEL_01 = 0xbad01, + DRAFT_00 = 0xff000000, + DRAFT_01 = 0xff000001, + DRAFT_02 = 0xff000002, + DRAFT_03 = 0xff000003, + DRAFT_04 = 0xff000004, + DRAFT_05 = 0xff000005, + DRAFT_06 = 0xff000006, + DRAFT_07 = 0xff000007, + DRAFT_14 = 0xff00000e, + DRAFT_16 = 0xff000010, + KIXEL_00 = 0xbad00, + KIXEL_01 = 0xbad01, } // Discriminated union where data type matches the control message type type MessageWithType = - | { type: ControlMessageType.Publish; message: Publish } - | { type: ControlMessageType.PublishOk; message: PublishOk } - | { type: ControlMessageType.PublishError; message: PublishError } - | { type: ControlMessageType.PublishDone; message: PublishDone } - | { type: ControlMessageType.PublishNamespace; message: PublishNamespace } - | { type: ControlMessageType.PublishNamespaceOk; message: PublishNamespaceOk } - | { type: ControlMessageType.PublishNamespaceError; message: PublishNamespaceError } - | { type: ControlMessageType.PublishNamespaceDone; message: PublishNamespaceDone } - | { type: ControlMessageType.Fetch; message: Fetch } - | { type: ControlMessageType.FetchOk; message: FetchOk } - | { type: ControlMessageType.FetchError; message: FetchError } - | { type: ControlMessageType.FetchCancel; message: FetchCancel } - | { type: ControlMessageType.Subscribe; message: Subscribe } - | { type: ControlMessageType.SubscribeOk; message: SubscribeOk } - | { type: ControlMessageType.SubscribeError; message: SubscribeError } - | { type: ControlMessageType.SubscribeUpdate; message: SubscribeUpdate } - | { type: ControlMessageType.SubscribeNamespace; message: SubscribeNamespace } - | { type: ControlMessageType.SubscribeNamespaceOk; message: SubscribeNamespaceOk } - | { type: ControlMessageType.SubscribeNamespaceError; message: SubscribeNamespaceError } - | { type: ControlMessageType.Unsubscribe; message: Unsubscribe } + | { type: ControlMessageType.Publish; message: Publish } + | { type: ControlMessageType.PublishOk; message: PublishOk } + | { type: ControlMessageType.PublishDone; message: PublishDone } + | { type: ControlMessageType.PublishNamespace; message: PublishNamespace } + | { type: ControlMessageType.PublishNamespaceDone; message: PublishNamespaceDone } + | { type: ControlMessageType.PublishNamespaceCancel; message: PublishNamespaceCancel } + | { type: ControlMessageType.Namespace; message: Namespace } + | { type: ControlMessageType.NamespaceDone; message: NamespaceDone } + | { type: ControlMessageType.TrackStatus; message: TrackStatus } + | { type: ControlMessageType.Fetch; message: Fetch } + | { type: ControlMessageType.FetchOk; message: FetchOk } + | { type: ControlMessageType.FetchCancel; message: FetchCancel } + | { type: ControlMessageType.Subscribe; message: Subscribe } + | { type: ControlMessageType.SubscribeOk; message: SubscribeOk } + | { type: ControlMessageType.SubscribeUpdate; message: SubscribeUpdate } + | { type: ControlMessageType.SubscribeNamespace; message: SubscribeNamespace } + | { type: ControlMessageType.Unsubscribe; message: Unsubscribe } + | { type: ControlMessageType.RequestOk; message: RequestOk } + | { type: ControlMessageType.RequestError; message: RequestError } + | { type: ControlMessageType.MaxRequestId; message: MaxRequestId } + | { type: ControlMessageType.RequestsBlocked; message: RequestsBlocked } type Message = Subscriber | Publisher // Sent by subscriber type Subscriber = Subscribe | SubscribeUpdate | SubscribeNamespace | - Unsubscribe | PublishOk | PublishError | - PublishNamespaceOk | PublishNamespaceError | Fetch | FetchCancel + Unsubscribe | PublishOk | Fetch | FetchCancel | PublishNamespaceCancel | + TrackStatus | MaxRequestId | RequestsBlocked | RequestOk | RequestError // Sent by publisher -type Publisher = SubscribeOk | SubscribeError - | SubscribeNamespaceOk | SubscribeNamespaceError | - PublishDone | Publish | PublishNamespace | PublishNamespaceDone - | FetchOk | FetchError +type Publisher = SubscribeOk | PublishDone | Publish | PublishNamespace | PublishNamespaceDone + | Namespace | NamespaceDone | FetchOk | MaxRequestId | RequestsBlocked | RequestOk | RequestError function isSubscriber(m: ControlMessageType): boolean { - return ( - m == ControlMessageType.Subscribe || - m == ControlMessageType.SubscribeUpdate || - m == ControlMessageType.Unsubscribe || - m == ControlMessageType.PublishOk || - m == ControlMessageType.PublishError || - m == ControlMessageType.PublishNamespaceOk || - m == ControlMessageType.PublishNamespaceError - ) + return ( + m == ControlMessageType.Subscribe || + m == ControlMessageType.SubscribeUpdate || + m == ControlMessageType.Unsubscribe || + m == ControlMessageType.PublishOk || + m == ControlMessageType.PublishNamespaceCancel || + m == ControlMessageType.TrackStatus || + m == ControlMessageType.MaxRequestId || + m == ControlMessageType.RequestsBlocked || + m == ControlMessageType.RequestOk || + m == ControlMessageType.RequestError + ) } function isPublisher(m: ControlMessageType): boolean { - return ( - m == ControlMessageType.SubscribeOk || - m == ControlMessageType.SubscribeError || - m == ControlMessageType.PublishDone || - m == ControlMessageType.Publish || - m == ControlMessageType.PublishNamespace || - m == ControlMessageType.PublishNamespaceDone - ) + return ( + m == ControlMessageType.SubscribeOk || + m == ControlMessageType.PublishDone || + m == ControlMessageType.Publish || + m == ControlMessageType.PublishNamespace || + m == ControlMessageType.PublishNamespaceDone || + m == ControlMessageType.Namespace || + m == ControlMessageType.NamespaceDone || + m == ControlMessageType.MaxRequestId || + m == ControlMessageType.RequestsBlocked || + m == ControlMessageType.RequestOk || + m == ControlMessageType.RequestError + ) } +// Draft-16: Control Message Types (Table 1) export enum ControlMessageType { - ReservedSetupV00 = 0x1, - GoAway = 0x10, - MaxRequestId = 0x15, - RequestsBlocked = 0x1a, - - SubscribeUpdate = 0x2, - Subscribe = 0x3, - SubscribeOk = 0x4, - SubscribeError = 0x5, - Unsubscribe = 0xa, - PublishDone = 0xb, - - Publish = 0x1d, - PublishOk = 0x1e, - PublishError = 0x1f, - PublishNamespace = 0x6, - PublishNamespaceOk = 0x7, - PublishNamespaceError = 0x8, - PublishNamespaceDone = 0x9, - SubscribeNamespace = 0x11, - SubscribeNamespaceOk = 0x12, - SubscribeNamespaceError = 0x13, - Fetch = 0x16, - FetchCancel = 0x17, - FetchOk = 0x18, - FetchError = 0x19, - - ClientSetup = 0x20, - ServerSetup = 0x21, + ReservedSetupV00 = 0x1, + + RequestUpdate = 0x2, + Subscribe = 0x3, + SubscribeOk = 0x4, + RequestError = 0x5, + PublishNamespace = 0x6, + RequestOk = 0x7, + Namespace = 0x8, + PublishNamespaceDone = 0x9, + Unsubscribe = 0xa, + PublishDone = 0xb, + PublishNamespaceCancel = 0xc, + TrackStatus = 0xd, + NamespaceDone = 0xe, + + GoAway = 0x10, + SubscribeNamespace = 0x11, + + MaxRequestId = 0x15, + Fetch = 0x16, + FetchCancel = 0x17, + FetchOk = 0x18, + RequestsBlocked = 0x1a, + + Publish = 0x1d, + PublishOk = 0x1e, + + ClientSetup = 0x20, + ServerSetup = 0x21, + + // Legacy aliases for backward compat during transition + SubscribeUpdate = 0x2, // Same as RequestUpdate in draft-16 } export namespace ControlMessageType { - export function toString(t: ControlMessageType): string { - switch (t) { - case ControlMessageType.ReservedSetupV00: return "ReservedSetupV00" - case ControlMessageType.GoAway: return "GoAway" - case ControlMessageType.MaxRequestId: return "MaxRequestId" - case ControlMessageType.RequestsBlocked: return "RequestsBlocked" - case ControlMessageType.SubscribeUpdate: return "SubscribeUpdate" - case ControlMessageType.Subscribe: return "Subscribe" - case ControlMessageType.SubscribeOk: return "SubscribeOk" - case ControlMessageType.SubscribeError: return "SubscribeError" - case ControlMessageType.Unsubscribe: return "Unsubscribe" - case ControlMessageType.PublishDone: return "PublishDone" - case ControlMessageType.Publish: return "Publish" - case ControlMessageType.PublishOk: return "PublishOk" - case ControlMessageType.PublishError: return "PublishError" - case ControlMessageType.PublishNamespace: return "PublishNamespace" - case ControlMessageType.PublishNamespaceOk: return "PublishNamespaceOk" - case ControlMessageType.PublishNamespaceError: return "PublishNamespaceError" - case ControlMessageType.PublishNamespaceDone: return "PublishNamespaceDone" - case ControlMessageType.SubscribeNamespace: return "SubscribeNamespace" - case ControlMessageType.SubscribeNamespaceOk: return "SubscribeNamespaceOk" - case ControlMessageType.SubscribeNamespaceError: return "SubscribeNamespaceError" - case ControlMessageType.Fetch: return "Fetch" - case ControlMessageType.FetchCancel: return "FetchCancel" - case ControlMessageType.FetchOk: return "FetchOk" - case ControlMessageType.FetchError: return "FetchError" - case ControlMessageType.ClientSetup: return "ClientSetup" - case ControlMessageType.ServerSetup: return "ServerSetup" - } - } + export function toString(t: ControlMessageType): string { + switch (t) { + case ControlMessageType.ReservedSetupV00: return "ReservedSetupV00" + case ControlMessageType.GoAway: return "GoAway" + case ControlMessageType.MaxRequestId: return "MaxRequestId" + case ControlMessageType.RequestsBlocked: return "RequestsBlocked" + case ControlMessageType.RequestUpdate: return "RequestUpdate" + case ControlMessageType.Subscribe: return "Subscribe" + case ControlMessageType.SubscribeOk: return "SubscribeOk" + case ControlMessageType.RequestError: return "RequestError" + case ControlMessageType.Unsubscribe: return "Unsubscribe" + case ControlMessageType.PublishDone: return "PublishDone" + case ControlMessageType.PublishNamespaceCancel: return "PublishNamespaceCancel" + case ControlMessageType.TrackStatus: return "TrackStatus" + case ControlMessageType.NamespaceDone: return "NamespaceDone" + case ControlMessageType.Publish: return "Publish" + case ControlMessageType.PublishOk: return "PublishOk" + case ControlMessageType.PublishNamespace: return "PublishNamespace" + case ControlMessageType.RequestOk: return "RequestOk" + case ControlMessageType.Namespace: return "Namespace" + case ControlMessageType.PublishNamespaceDone: return "PublishNamespaceDone" + case ControlMessageType.SubscribeNamespace: return "SubscribeNamespace" + case ControlMessageType.Fetch: return "Fetch" + case ControlMessageType.FetchCancel: return "FetchCancel" + case ControlMessageType.FetchOk: return "FetchOk" + case ControlMessageType.ClientSetup: return "ClientSetup" + case ControlMessageType.ServerSetup: return "ServerSetup" + } + } } export { - Subscribe, - SubscribeOk, - SubscribeError, - SubscribeUpdate, - SubscribeNamespace, - SubscribeNamespaceOk, - SubscribeNamespaceError, - Unsubscribe, - Publish, - PublishOk, - PublishError, - PublishDone, - PublishNamespace, - PublishNamespaceOk, - PublishNamespaceError, - PublishNamespaceDone, - Fetch, - FetchOk, - FetchError, - FetchCancel, - GoAway, - ClientSetup, - ServerSetup, - - Version, - isSubscriber, - isPublisher, - MessageWithType, - Message, - GroupOrder, - FilterType, -} \ No newline at end of file + Subscribe, + SubscribeOk, + SubscribeUpdate, + SubscribeNamespace, + SubscribeOptions, + Unsubscribe, + Publish, + PublishOk, + PublishDone, + PublishNamespace, + PublishNamespaceDone, + PublishNamespaceCancel, + Namespace, + NamespaceDone, + TrackStatus, + Fetch, + FetchOk, + FetchCancel, + GoAway, + ClientSetup, + ServerSetup, + MaxRequestId, + RequestsBlocked, + RequestOk, + RequestError, + + Version, + isSubscriber, + isPublisher, + MessageWithType, + Message, + GroupOrder, + FilterType, +} diff --git a/lib/transport/control/max_request_id.ts b/lib/transport/control/max_request_id.ts index 5311b75..2c58538 100644 --- a/lib/transport/control/max_request_id.ts +++ b/lib/transport/control/max_request_id.ts @@ -2,8 +2,11 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +// Draft-16: MAX_REQUEST_ID (Section 9.5, type 0x15) +// Sent to increase the number of requests the peer can send within a session. +// The Maximum Request ID MUST only increase within a session. export interface MaxRequestId { - id: bigint + max_request_id: bigint } export namespace MaxRequestId { @@ -11,17 +14,16 @@ export namespace MaxRequestId { const mainBuf = new MutableBytesBuffer(new Uint8Array()) mainBuf.putVarInt(ControlMessageType.MaxRequestId) const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - + payloadBuf.putVarInt(v.max_request_id) mainBuf.putU16(payloadBuf.byteLength) mainBuf.putBytes(payloadBuf.Uint8Array) return mainBuf.Uint8Array } export function deserialize(reader: ImmutableBytesBuffer): MaxRequestId { - const id = reader.getVarInt() + const max_request_id = reader.getVarInt() return { - id, + max_request_id, } } } \ No newline at end of file diff --git a/lib/transport/control/namespace.ts b/lib/transport/control/namespace.ts new file mode 100644 index 0000000..a381645 --- /dev/null +++ b/lib/transport/control/namespace.ts @@ -0,0 +1,27 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Tuple } from "../base_data" + + +export interface Namespace { + namespace_suffix: string[] +} + +export namespace Namespace { + export function serialize(v: Namespace): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Namespace) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putBytes(Tuple.serialize(v.namespace_suffix)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): Namespace { + const namespace_suffix = Tuple.deserialize(reader) + return { + namespace_suffix, + } + } +} diff --git a/lib/transport/control/namespace_done.ts b/lib/transport/control/namespace_done.ts new file mode 100644 index 0000000..66da359 --- /dev/null +++ b/lib/transport/control/namespace_done.ts @@ -0,0 +1,27 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Tuple } from "../base_data" + + +export interface NamespaceDone { + namespace_suffix: string[] +} + +export namespace NamespaceDone { + export function serialize(v: NamespaceDone): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.NamespaceDone) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putBytes(Tuple.serialize(v.namespace_suffix)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): NamespaceDone { + const namespace_suffix = Tuple.deserialize(reader) + return { + namespace_suffix, + } + } +} diff --git a/lib/transport/control/publish.ts b/lib/transport/control/publish.ts index 5acd9d0..383ece1 100644 --- a/lib/transport/control/publish.ts +++ b/lib/transport/control/publish.ts @@ -1,61 +1,61 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Location, Tuple } from "../base_data" +import { Parameters, Location, Tuple, KeyValuePairs } from "../base_data" export interface Publish { - id: bigint - track_alias: bigint // Publisher-specified - namespace: Tuple - name: string - content_exists: number // 0 or 1 - group_order: GroupOrder - largest_location?: Location // largest location of group or object if content_exists == 1 - forward: number // 0 or 1 - params?: Parameters + id: bigint + track_alias: bigint // Publisher-specified + namespace: Tuple + name: string + track_extensions?: KeyValuePairs + params?: Parameters } - export namespace Publish { - export function serialize(v: Publish): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Publish) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.track_alias) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putUtf8String(v.name) - payloadBuf.putU8(v.content_exists) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - if (v.largest_location) { - payloadBuf.putBytes(Location.serialize(v.largest_location)) - } - payloadBuf.putU8(v.forward) - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: Publish): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Publish) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + payloadBuf.putVarInt(v.track_alias) + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) + const extensions = v.track_extensions ?? new Map() + const extBytes = KeyValuePairs.serialize(extensions) + payloadBuf.putVarInt(BigInt(extBytes.length)) + payloadBuf.putBytes(extBytes) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): Publish { - const id = reader.getVarInt() - const track_alias = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - const name = reader.getUtf8String() - const content_exists = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const largest_location = Location.deserialize(reader) - const forward = reader.getU8() - const params = Parameters.deserialize(reader) - return { - id, - track_alias, - namespace, - name, - content_exists, - group_order, - largest_location, - forward, - params - } - } + export function deserialize(reader: ImmutableBytesBuffer): Publish { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const name = reader.getUtf8String() + const track_alias = reader.getVarInt() + const numParams = reader.getNumberVarInt() + let params: Parameters | undefined + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + const extLength = reader.getNumberVarInt() + let track_extensions: KeyValuePairs | undefined + if (extLength > 0) { + const extData = reader.getBytes(Number(extLength)) + track_extensions = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extData)) + } + return { + id, + track_alias, + namespace, + name, + track_extensions, + params + } + } } diff --git a/lib/transport/control/publish_done.ts b/lib/transport/control/publish_done.ts index f2927fa..ccb6aa8 100644 --- a/lib/transport/control/publish_done.ts +++ b/lib/transport/control/publish_done.ts @@ -1,39 +1,40 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { ReasonPhrase } from "../base_data" export interface PublishDone { - id: bigint - code: bigint - stream_count: bigint - reason: string + id: bigint + code: bigint + stream_count: bigint + reason: string } export namespace PublishDone { - export function serialize(v: PublishDone): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishDone) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putVarInt(v.stream_count) - payloadBuf.putUtf8String(v.reason) + export function serialize(v: PublishDone): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishDone) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.code) + payloadBuf.putVarInt(v.stream_count) + payloadBuf.putBytes(ReasonPhrase.serialize(v.reason)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): PublishDone { - const id = reader.getVarInt() - const code = reader.getVarInt() - const stream_count = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - stream_count, - reason - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishDone { + const id = reader.getVarInt() + const code = reader.getVarInt() + const stream_count = reader.getVarInt() + const reason = ReasonPhrase.deserialize(reader) + return { + id, + code, + stream_count, + reason + } + } +} diff --git a/lib/transport/control/publish_error.ts b/lib/transport/control/publish_error.ts deleted file mode 100644 index ba25253..0000000 --- a/lib/transport/control/publish_error.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface PublishError { - id: bigint - code: bigint - reason: string -} - -export namespace PublishError { - export function serialize(v: PublishError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/publish_namespace.ts b/lib/transport/control/publish_namespace.ts index a610322..ac13926 100644 --- a/lib/transport/control/publish_namespace.ts +++ b/lib/transport/control/publish_namespace.ts @@ -1,37 +1,42 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Tuple } from "../base_data" +import { Parameters, Tuple, KeyValuePairs } from "../base_data" export interface PublishNamespace { - id: bigint - namespace: Tuple - params?: Parameters + id: bigint + namespace: Tuple + params?: Parameters } export namespace PublishNamespace { - export function serialize(v: PublishNamespace): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespace) + export function serialize(v: PublishNamespace): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishNamespace) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + // Draft-16: Number of Parameters + delta-encoded parameters + const params = v.params ?? new Map() + const paramsBytes = KeyValuePairs.serialize(params) + payloadBuf.putVarInt(params.size) + payloadBuf.putBytes(paramsBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespace { - const id = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - const params = Parameters.deserialize(reader) - return { - id, - namespace, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishNamespace { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + namespace, + params + } + } +} diff --git a/lib/transport/control/publish_namespace_cancel.ts b/lib/transport/control/publish_namespace_cancel.ts new file mode 100644 index 0000000..7640cd5 --- /dev/null +++ b/lib/transport/control/publish_namespace_cancel.ts @@ -0,0 +1,35 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { ReasonPhrase } from "../base_data" + + +export interface PublishNamespaceCancel { + id: bigint + error_code: bigint + error_reason: string +} + +export namespace PublishNamespaceCancel { + export function serialize(v: PublishNamespaceCancel): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishNamespaceCancel) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.error_code) + payloadBuf.putBytes(ReasonPhrase.serialize(v.error_reason)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceCancel { + const id = reader.getVarInt() + const error_code = reader.getVarInt() + const error_reason = ReasonPhrase.deserialize(reader) + return { + id, + error_code, + error_reason + } + } +} diff --git a/lib/transport/control/publish_namespace_done.ts b/lib/transport/control/publish_namespace_done.ts index 9c5b737..127b4d4 100644 --- a/lib/transport/control/publish_namespace_done.ts +++ b/lib/transport/control/publish_namespace_done.ts @@ -1,27 +1,25 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Tuple } from "../base_data" export interface PublishNamespaceDone { - namespace: Tuple + id: bigint } export namespace PublishNamespaceDone { - export function serialize(v: PublishNamespaceDone): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespaceDone) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) + export function serialize(v: PublishNamespaceDone): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishNamespaceDone) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceDone { - const namespace = Tuple.deserialize(reader) - return { - namespace, - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceDone { + const id = reader.getVarInt() + return { + id, + } + } +} diff --git a/lib/transport/control/publish_namespace_error.ts b/lib/transport/control/publish_namespace_error.ts deleted file mode 100644 index 8c388e4..0000000 --- a/lib/transport/control/publish_namespace_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - - -export interface PublishNamespaceError { - id: bigint - code: bigint - reason: string -} - -export namespace PublishNamespaceError { - export function serialize(v: PublishNamespaceError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespaceError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/publish_namespace_ok.ts b/lib/transport/control/publish_namespace_ok.ts deleted file mode 100644 index ce831a4..0000000 --- a/lib/transport/control/publish_namespace_ok.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - -export interface PublishNamespaceOk { - id: bigint -} - -export namespace PublishNamespaceOk { - export function serialize(v: PublishNamespaceOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishNamespaceOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): PublishNamespaceOk { - const id = reader.getVarInt() - return { - id - } - } -} \ No newline at end of file diff --git a/lib/transport/control/publish_ok.ts b/lib/transport/control/publish_ok.ts index c233c19..d94fbf0 100644 --- a/lib/transport/control/publish_ok.ts +++ b/lib/transport/control/publish_ok.ts @@ -1,70 +1,38 @@ -import { ControlMessageType, FilterType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Location, Parameters } from "../base_data" +import { Parameters } from "../base_data" export interface PublishOk { - id: bigint // Request ID - forward: number // 0 or 1 u8 - subscriber_priority: number // u8 - group_order: GroupOrder - filter_type: FilterType - start_location?: Location - end_group?: bigint - params?: Parameters + id: bigint // Request ID + params?: Parameters } - export namespace PublishOk { - export function serialize(v: PublishOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.PublishOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putU8(v.forward) - payloadBuf.putU8(v.subscriber_priority) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putBytes(FilterType.serialize(v.filter_type)) - if (v.start_location) { - payloadBuf.putBytes(Location.serialize(v.start_location)) - } - if (v.end_group) { - payloadBuf.putVarInt(v.end_group) - } - payloadBuf.putBytes(Parameters.serialize(v.params ?? new Map())) + export function serialize(v: PublishOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.PublishOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + const params = v.params ?? new Map() + payloadBuf.putVarInt(BigInt(params.size)) + payloadBuf.putBytes(Parameters.serialize(params)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): PublishOk { - const id = reader.getVarInt() - const forward = reader.getU8() - const subscriber_priority = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const filter_type = FilterType.deserialize(reader) - let start_location: Location | undefined = undefined - if (filter_type == FilterType.AbsoluteStart || filter_type == FilterType.AbsoluteRange) { - start_location = Location.deserialize(reader) - } - let end_group: bigint | undefined = undefined - if (filter_type == FilterType.AbsoluteRange) { - end_group = reader.getVarInt() - } - let params: Parameters | undefined = undefined - if (reader.remaining > 0) { - params = Parameters.deserialize(reader) - } - return { - id, - forward, - subscriber_priority, - group_order, - filter_type, - start_location, - end_group, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): PublishOk { + const id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + let params: Parameters | undefined + if (numParams > 0) { + params = Parameters.deserialize_with_count(reader, Number(numParams)) + } + return { + id, + params + } + } +} diff --git a/lib/transport/control/request_error.ts b/lib/transport/control/request_error.ts new file mode 100644 index 0000000..75fb6f3 --- /dev/null +++ b/lib/transport/control/request_error.ts @@ -0,0 +1,41 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { ReasonPhrase } from "../base_data" + +// Draft-16: REQUEST_ERROR (Section 9.8) +// Sent in response to any request (SUBSCRIBE, FETCH, PUBLISH, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS) +export interface RequestError { + id: bigint // Request ID + code: bigint // Error Code (RequestErrorCode) + retry_interval: bigint // Minimum retry time in ms + 1; 0 = don't retry + reason: ReasonPhrase +} + +export namespace RequestError { + export function serialize(v: RequestError): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestError) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.code) + payloadBuf.putVarInt(v.retry_interval) + payloadBuf.putBytes(ReasonPhrase.serialize(v.reason)) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): RequestError { + const id = reader.getVarInt() + const code = reader.getVarInt() + const retry_interval = reader.getVarInt() + const reason = ReasonPhrase.deserialize(reader) + return { + id, + code, + retry_interval, + reason, + } + } +} diff --git a/lib/transport/control/request_ok.ts b/lib/transport/control/request_ok.ts new file mode 100644 index 0000000..0017d28 --- /dev/null +++ b/lib/transport/control/request_ok.ts @@ -0,0 +1,36 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Parameters } from "../base_data" + +// Draft-16: REQUEST_OK (Section 9.7) +// Sent in response to REQUEST_UPDATE, TRACK_STATUS, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE +export interface RequestOk { + id: bigint // Request ID + parameters: Parameters +} + +export namespace RequestOk { + export function serialize(v: RequestOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + const paramsBytes = Parameters.serialize(v.parameters) + payloadBuf.putVarInt(v.parameters.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): RequestOk { + const id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const parameters = Parameters.deserialize_with_count(reader, numParams) + return { + id, + parameters, + } + } +} diff --git a/lib/transport/control/requests_block.ts b/lib/transport/control/requests_block.ts deleted file mode 100644 index 9d9beb7..0000000 --- a/lib/transport/control/requests_block.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface RequestsBlocked { - id: bigint -} - -export namespace RequestsBlocked { - export function serialize(v: RequestsBlocked): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.RequestsBlocked) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): RequestsBlocked { - const id = reader.getVarInt() - return { - id, - } - } -} \ No newline at end of file diff --git a/lib/transport/control/requests_blocked.ts b/lib/transport/control/requests_blocked.ts new file mode 100644 index 0000000..10050b0 --- /dev/null +++ b/lib/transport/control/requests_blocked.ts @@ -0,0 +1,30 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" + + +// Draft-16: REQUESTS_BLOCKED (Section 9.6, type 0x1A) +// Sent when an endpoint would like to send a new request, but cannot +// because the Request ID would exceed the Maximum Request ID value sent by the peer. +export interface RequestsBlocked { + maximum_request_id: bigint +} + +export namespace RequestsBlocked { + export function serialize(v: RequestsBlocked): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestsBlocked) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.maximum_request_id) + + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): RequestsBlocked { + const maximum_request_id = reader.getVarInt() + return { + maximum_request_id, + } + } +} diff --git a/lib/transport/control/server_setup.ts b/lib/transport/control/server_setup.ts index 2f531a3..550c954 100644 --- a/lib/transport/control/server_setup.ts +++ b/lib/transport/control/server_setup.ts @@ -3,27 +3,28 @@ import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters } from "../base_data" export interface ServerSetup { - version: Version - params: Parameters + params: Parameters } export namespace ServerSetup { - export function serialize(v: ServerSetup): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.ServerSetup) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.version) - payloadBuf.putBytes(Parameters.serialize(v.params)) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: ServerSetup): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.ServerSetup) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) + payloadBuf.putBytes(paramsBytes) - export function deserialize(reader: ImmutableBytesBuffer): ServerSetup { - const version = reader.getNumberVarInt() as Version - return { - version, - params: Parameters.deserialize(reader) - } - } -} \ No newline at end of file + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): ServerSetup { + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + params + } + } +} diff --git a/lib/transport/control/subscribe.ts b/lib/transport/control/subscribe.ts index 9ea3314..e04ae81 100644 --- a/lib/transport/control/subscribe.ts +++ b/lib/transport/control/subscribe.ts @@ -1,143 +1,104 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Tuple, Parameters, Location, KeyValuePairs } from "../base_data" -import { debug } from "../utils" +import { Tuple, Parameters } from "../base_data" export enum GroupOrder { - Publisher = 0x0, - Ascending = 0x1, - Descending = 0x2, + Publisher = 0x0, + Ascending = 0x1, + Descending = 0x2, } export namespace GroupOrder { - export function serialize(v: GroupOrder): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putU8(v) - return buf.Uint8Array - } + export function serialize(v: GroupOrder): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putU8(v) + return buf.Uint8Array + } - export function deserialize(buffer: ImmutableBytesBuffer): GroupOrder { - const order = buffer.getU8() - switch (order) { - case 0: - return GroupOrder.Publisher - case 1: - return GroupOrder.Ascending - case 2: - return GroupOrder.Descending - default: - throw new Error(`Invalid GroupOrder value: ${order}`) - } - } + export function deserialize(buffer: ImmutableBytesBuffer): GroupOrder { + const order = buffer.getU8() + switch (order) { + case 0: + return GroupOrder.Publisher + case 1: + return GroupOrder.Ascending + case 2: + return GroupOrder.Descending + default: + throw new Error(`Invalid GroupOrder value: ${order}`) + } + } } export enum FilterType { - NextGroupStart = 0x1, - LargestObject = 0x2, - AbsoluteStart = 0x3, - AbsoluteRange = 0x4, + NextGroupStart = 0x1, + LargestObject = 0x2, + AbsoluteStart = 0x3, + AbsoluteRange = 0x4, } export namespace FilterType { - export function serialize(v: FilterType): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(v) - return buf.Uint8Array - } + export function serialize(v: FilterType): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(v) + return buf.Uint8Array + } - export function deserialize(buffer: ImmutableBytesBuffer): FilterType { - const order = buffer.getVarInt() - switch (order) { - case 1n: - return FilterType.NextGroupStart - case 2n: - return FilterType.LargestObject - case 3n: - return FilterType.AbsoluteStart - case 4n: - return FilterType.AbsoluteRange - default: - throw new Error(`Invalid FilterType value: ${order}`) - } - } + export function deserialize(buffer: ImmutableBytesBuffer): FilterType { + const order = buffer.getVarInt() + switch (order) { + case 1n: + return FilterType.NextGroupStart + case 2n: + return FilterType.LargestObject + case 3n: + return FilterType.AbsoluteStart + case 4n: + return FilterType.AbsoluteRange + default: + throw new Error(`Invalid FilterType value: ${order}`) + } + } } export interface Subscribe { - id: bigint // Request ID in draft-14 - namespace: Tuple, - name: string - subscriber_priority: number // u8 - group_order: GroupOrder - forward: number // u8 -> 0/1 - filter_type: FilterType - start_location?: Location - end_group?: bigint - params: Parameters + id: bigint // Request ID + namespace: Tuple, + name: string + params: Parameters } export namespace Subscribe { - export function serialize(v: Subscribe): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Subscribe) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putUtf8String(v.name) - payloadBuf.putU8(v.subscriber_priority) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putU8(v.forward) - payloadBuf.putBytes(FilterType.serialize(v.filter_type)) + export function serialize(v: Subscribe): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Subscribe) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + // Draft-16: inline parameters moved to Parameters KVP + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) - if (v.filter_type === FilterType.AbsoluteStart || v.filter_type === FilterType.AbsoluteRange) { - if (!v.start_location) { - throw new Error('start location required for absolute start or absolute range') - } - payloadBuf.putBytes(Location.serialize(v.start_location)) - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - if (v.filter_type === FilterType.AbsoluteRange) { - if (v.end_group == null) { - throw new Error('end group required for absolute range') - } - payloadBuf.putVarInt(v.end_group) - } - payloadBuf.putBytes(Parameters.serialize(v.params)) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): Subscribe { - const id = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - const name = reader.getUtf8String() - const subscriber_priority = reader.getU8() - const group_order = GroupOrder.deserialize(reader) - const forward = reader.getU8() - const filter_type = FilterType.deserialize(reader) - let start_location: Location | undefined - if (filter_type == FilterType.AbsoluteRange || filter_type == FilterType.AbsoluteStart) { - start_location = Location.deserialize(reader) - } - let end_group: bigint | undefined - if (filter_type == FilterType.AbsoluteRange) { - end_group = reader.getVarInt() - } - const params = Parameters.deserialize(reader) - return { - id, - namespace, - name, - subscriber_priority, - group_order, - forward, - filter_type, - start_location, - end_group, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): Subscribe { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const name = reader.getUtf8String() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + namespace, + name, + params, + } + } +} diff --git a/lib/transport/control/subscribe_error.ts b/lib/transport/control/subscribe_error.ts deleted file mode 100644 index c2121a3..0000000 --- a/lib/transport/control/subscribe_error.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - -export interface SubscribeError { - id: bigint // Request ID in draft-14 - code: bigint - reason: string -} - - -export namespace SubscribeError { - export function serialize(v: SubscribeError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/subscribe_namespace.ts b/lib/transport/control/subscribe_namespace.ts index 605e716..d930c51 100644 --- a/lib/transport/control/subscribe_namespace.ts +++ b/lib/transport/control/subscribe_namespace.ts @@ -1,39 +1,49 @@ -import { ControlMessageType, FilterType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Tuple, Parameters, Location, KeyValuePairs } from "../base_data" +import { Tuple, Parameters, KeyValuePairs } from "../base_data" +export enum SubscribeOptions { + PUBLISH = 0x00, + NAMESPACE = 0x01, + BOTH = 0x02, +} + export interface SubscribeNamespace { - id: bigint, - namespace: string[] - params?: Parameters + id: bigint, + namespace: string[] + subscribe_options: SubscribeOptions + params?: Parameters } export namespace SubscribeNamespace { - export function serialize(v: SubscribeNamespace): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeNamespace) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putBytes(Tuple.serialize(v.namespace)) - payloadBuf.putBytes(KeyValuePairs.serialize(v.params ?? new Map())) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: SubscribeNamespace): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.SubscribeNamespace) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putVarInt(v.subscribe_options) + payloadBuf.putBytes(KeyValuePairs.serialize(v.params ?? new Map())) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespace { - const id = reader.getVarInt() - const namespace = Tuple.deserialize(reader) - let params: Parameters | undefined - if (reader.remaining > 0) { - params = KeyValuePairs.deserialize(reader) - } - return { - id, - namespace, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespace { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const subscribe_options = reader.getNumberVarInt() as SubscribeOptions + let params: Parameters | undefined + if (reader.remaining > 0) { + params = KeyValuePairs.deserialize(reader) + } + return { + id, + namespace, + subscribe_options, + params + } + } +} diff --git a/lib/transport/control/subscribe_namespace_error.ts b/lib/transport/control/subscribe_namespace_error.ts deleted file mode 100644 index f1d24f6..0000000 --- a/lib/transport/control/subscribe_namespace_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - -export interface SubscribeNamespaceError { - id: bigint, - code: bigint, - reason: string -} - - -export namespace SubscribeNamespaceError { - export function serialize(v: SubscribeNamespaceError): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeNamespaceError) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.code) - payloadBuf.putUtf8String(v.reason) - - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespaceError { - const id = reader.getVarInt() - const code = reader.getVarInt() - const reason = reader.getUtf8String() - return { - id, - code, - reason - } - } -} \ No newline at end of file diff --git a/lib/transport/control/subscribe_namespace_ok.ts b/lib/transport/control/subscribe_namespace_ok.ts deleted file mode 100644 index 3da6031..0000000 --- a/lib/transport/control/subscribe_namespace_ok.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ControlMessageType } from "." -import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - - - -export interface SubscribeNamespaceOk { - id: bigint, -} - - -export namespace SubscribeNamespaceOk { - export function serialize(v: SubscribeNamespaceOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeNamespaceOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeNamespaceOk { - const id = reader.getVarInt() - return { - id, - } - } -} \ No newline at end of file diff --git a/lib/transport/control/subscribe_ok.ts b/lib/transport/control/subscribe_ok.ts index 91b6c98..50637bd 100644 --- a/lib/transport/control/subscribe_ok.ts +++ b/lib/transport/control/subscribe_ok.ts @@ -1,64 +1,58 @@ import { ControlMessageType } from "." import { GroupOrder } from "./subscribe" import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, Location } from "../base_data" +import { Parameters, KeyValuePairs } from "../base_data" export interface SubscribeOk { - id: bigint // Request ID - track_alias: bigint // Publisher-specified in draft-14 - expires: bigint - group_order: GroupOrder - content_exists: number // 0 or 1 - largest_location?: Location - params: Parameters + id: bigint // Request ID + track_alias: bigint + params: Parameters + track_extensions?: KeyValuePairs } export namespace SubscribeOk { - export function serialize(v: SubscribeOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.track_alias) - payloadBuf.putVarInt(v.expires) - payloadBuf.putBytes(GroupOrder.serialize(v.group_order)) - payloadBuf.putU8(v.content_exists) - if (v.content_exists) { - if (!v.largest_location) { - throw new Error('largest_location required for content_exists') - } - payloadBuf.putBytes(Location.serialize(v.largest_location)) - } + export function serialize(v: SubscribeOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.SubscribeOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.track_alias) + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) + // Draft-16: Track Extensions (length-prefixed KVP block) + if (v.track_extensions && v.track_extensions.size > 0) { + const extBytes = KeyValuePairs.serialize(v.track_extensions) + payloadBuf.putVarInt(extBytes.length) + payloadBuf.putBytes(extBytes) + } else { + payloadBuf.putVarInt(0) // empty track extensions + } - payloadBuf.putBytes(Parameters.serialize(v.params)) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): SubscribeOk { - const id = reader.getVarInt() - const track_alias = reader.getVarInt() - const expires = reader.getVarInt() - const group_order = GroupOrder.deserialize(reader) - const content_exists = reader.getNumberVarInt() - if (content_exists != 0 && content_exists != 1) { - throw new Error("Invalid content_exists value") - } - let largest_location: Location | undefined - if (content_exists) { - largest_location = Location.deserialize(reader) - } - const params = Parameters.deserialize(reader) - return { - id, - track_alias, - expires, - group_order, - content_exists, - largest_location, - params - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): SubscribeOk { + const id = reader.getVarInt() + const track_alias = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + // Draft-16: Track Extensions + let track_extensions: KeyValuePairs | undefined + if (reader.remaining > 0) { + const extLength = reader.getNumberVarInt() + if (extLength > 0) { + const extData = reader.getBytes(extLength) + track_extensions = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extData)) + } + } + return { + id, + track_alias, + params, + track_extensions, + } + } +} diff --git a/lib/transport/control/subscribe_update.ts b/lib/transport/control/subscribe_update.ts index 19f9405..82ed7d3 100644 --- a/lib/transport/control/subscribe_update.ts +++ b/lib/transport/control/subscribe_update.ts @@ -1,56 +1,41 @@ -import { ControlMessageType, GroupOrder } from "." +import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" -import { Parameters, KeyValuePairs, Location } from "../base_data" +import { Parameters, Location } from "../base_data" export interface SubscribeUpdate { - id: bigint - subscription_id: bigint - start_location: Location - end_group: bigint - subscriber_priority: number - forward: number - params?: Parameters + id: bigint // Request ID (new) + subscription_id: bigint // Existing Request ID + params?: Parameters } export namespace SubscribeUpdate { - export function serialize(v: SubscribeUpdate): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.SubscribeUpdate) + export function serialize(v: SubscribeUpdate): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.SubscribeUpdate) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - payloadBuf.putVarInt(v.subscription_id) - payloadBuf.putBytes(Location.serialize(v.start_location)) - payloadBuf.putVarInt(v.end_group) - payloadBuf.putVarInt(v.subscriber_priority) - payloadBuf.putVarInt(v.forward) - payloadBuf.putBytes(KeyValuePairs.serialize(v.params ?? new Map())) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putVarInt(v.subscription_id) // Existing Request ID + const params = v.params ?? new Map() + const paramsBytes = Parameters.serialize(params) + payloadBuf.putVarInt(params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): SubscribeUpdate { - const id = reader.getVarInt() - const subscription_id = reader.getVarInt() - const start_location = Location.deserialize(reader) - const end_group = reader.getVarInt() - const subscriber_priority = reader.getNumberVarInt() - const forward = reader.getNumberVarInt() - let params: Parameters | undefined - if (reader.remaining > 0) { - params = KeyValuePairs.deserialize(reader) - } - return { - id, - subscription_id, - start_location, - end_group, - subscriber_priority, - forward, - params - } - } + export function deserialize(reader: ImmutableBytesBuffer): SubscribeUpdate { + const id = reader.getVarInt() + const subscription_id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + subscription_id, + params, + } + } } diff --git a/lib/transport/control/track_status.ts b/lib/transport/control/track_status.ts new file mode 100644 index 0000000..4dab7e3 --- /dev/null +++ b/lib/transport/control/track_status.ts @@ -0,0 +1,42 @@ +import { ControlMessageType } from "." +import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" +import { Tuple, Parameters } from "../base_data" + + +export interface TrackStatus { + id: bigint + namespace: Tuple + name: string + params: Parameters +} + +export namespace TrackStatus { + export function serialize(v: TrackStatus): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.TrackStatus) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + payloadBuf.putBytes(Tuple.serialize(v.namespace)) + payloadBuf.putUtf8String(v.name) + const paramsBytes = Parameters.serialize(v.params) + payloadBuf.putVarInt(v.params.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } + + export function deserialize(reader: ImmutableBytesBuffer): TrackStatus { + const id = reader.getVarInt() + const namespace = Tuple.deserialize(reader) + const name = reader.getUtf8String() + const numParams = reader.getNumberVarInt() + const params = Parameters.deserialize_with_count(reader, numParams) + return { + id, + namespace, + name, + params, + } + } +} diff --git a/lib/transport/objects.ts b/lib/transport/objects.ts index e93846a..c8aa534 100644 --- a/lib/transport/objects.ts +++ b/lib/transport/objects.ts @@ -10,7 +10,6 @@ export enum ObjectForwardingPreference { export enum Status { NORMAL = 0, - OBJECT_NULL = 1, GROUP_END = 3, TRACK_END = 4, } @@ -30,8 +29,6 @@ export namespace Status { switch (v) { case 0: return Status.NORMAL - case 1: - return Status.OBJECT_NULL case 3: return Status.GROUP_END case 4: @@ -56,10 +53,20 @@ export interface Object { } export function isDatagram(obj: ObjectDatagram | SubgroupHeader): boolean { - return obj.type in ObjectDatagramType + // Datagram types have bit 4 NOT set; subgroup types have bit 4 SET + return (obj.type & 0x10) === 0 } +// Draft-16: Object Datagram types use bitmask structure 0b00X0XXXX +// Valid ranges: 0x00..0x0F, 0x20..0x2F +// Bit layout: +// bit 0 (0x01) = EXTENSIONS +// bit 1 (0x02) = END_OF_GROUP +// bit 2 (0x04) = ZERO_OBJECT_ID (when set, Object ID omitted, implied 0) +// bit 3 (0x08) = DEFAULT_PRIORITY (when set, priority field omitted) +// bit 5 (0x20) = STATUS (when set, Object Status present instead of payload) +// Invalid combinations: STATUS + END_OF_GROUP (0x22,0x23,0x26,0x27,0x2A,0x2B,0x2E,0x2F) export enum ObjectDatagramType { Type0x0 = 0x0, Type0x1 = 0x1, @@ -69,11 +76,32 @@ export enum ObjectDatagramType { Type0x5 = 0x5, Type0x6 = 0x6, Type0x7 = 0x7, + Type0x8 = 0x8, + Type0x9 = 0x9, + Type0xA = 0xa, + Type0xB = 0xb, + Type0xC = 0xc, + Type0xD = 0xd, + Type0xE = 0xe, + Type0xF = 0xf, Type0x20 = 0x20, Type0x21 = 0x21, + Type0x24 = 0x24, + Type0x25 = 0x25, + Type0x28 = 0x28, + Type0x29 = 0x29, + Type0x2C = 0x2c, + Type0x2D = 0x2d, } export namespace ObjectDatagramType { + // Bitmask constants + const EXTENSIONS_BIT = 0x01 + const END_OF_GROUP_BIT = 0x02 + const ZERO_OBJECT_ID_BIT = 0x04 + const DEFAULT_PRIORITY_BIT = 0x08 + const STATUS_BIT = 0x20 + export function serialize(type: ObjectDatagramType): Uint8Array { const w = new MutableBytesBuffer(new Uint8Array()) w.putVarInt(type) @@ -85,68 +113,40 @@ export namespace ObjectDatagramType { export function try_from(value: number | bigint): ObjectDatagramType { const v = typeof value === "bigint" ? Number(value) : value - switch (v) { - case ObjectDatagramType.Type0x0: - case ObjectDatagramType.Type0x1: - case ObjectDatagramType.Type0x2: - case ObjectDatagramType.Type0x3: - case ObjectDatagramType.Type0x4: - case ObjectDatagramType.Type0x5: - case ObjectDatagramType.Type0x6: - case ObjectDatagramType.Type0x7: - case ObjectDatagramType.Type0x20: - case ObjectDatagramType.Type0x21: - return v as ObjectDatagramType - default: - throw new Error(`invalid object datagram type: ${v}`) + // Must match form 0b00X0XXXX (bit 4 NOT set) + if ((v & 0x10) !== 0) { + throw new Error(`invalid object datagram type: ${v} (bit 4 set - this is a subgroup type)`) + } + // Must be in ranges 0x00..0x0F or 0x20..0x2F + if (v < 0x00 || (v > 0x0F && v < 0x20) || v > 0x2F) { + throw new Error(`invalid object datagram type: ${v} (out of range)`) } + // STATUS + END_OF_GROUP is invalid + if ((v & STATUS_BIT) !== 0 && (v & END_OF_GROUP_BIT) !== 0) { + throw new Error(`invalid object datagram type: ${v} (STATUS + END_OF_GROUP combination)`) + } + return v as ObjectDatagramType } export function isEndOfGroup(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x2: - case ObjectDatagramType.Type0x3: - case ObjectDatagramType.Type0x6: - case ObjectDatagramType.Type0x7: - return true - default: - return false - } + return (type & END_OF_GROUP_BIT) !== 0 } export function hasExtensions(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x1: - case ObjectDatagramType.Type0x3: - case ObjectDatagramType.Type0x5: - case ObjectDatagramType.Type0x7: - case ObjectDatagramType.Type0x21: - return true - default: - return false - } + return (type & EXTENSIONS_BIT) !== 0 } export function hasObjectId(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x4: - case ObjectDatagramType.Type0x5: - case ObjectDatagramType.Type0x6: - case ObjectDatagramType.Type0x7: - return false - default: - return true - } + // When ZERO_OBJECT_ID bit is set, Object ID is omitted (implied 0) + return (type & ZERO_OBJECT_ID_BIT) === 0 + } + + export function hasDefaultPriority(type: ObjectDatagramType) { + return (type & DEFAULT_PRIORITY_BIT) !== 0 } export function hasStatus(type: ObjectDatagramType) { - switch (type) { - case ObjectDatagramType.Type0x20: - case ObjectDatagramType.Type0x21: - return true - default: - return false - } + return (type & STATUS_BIT) !== 0 } } @@ -155,7 +155,7 @@ export interface ObjectDatagram { track_alias: bigint group_id: number object_id?: number - publisher_priority: number + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set extension_headers?: KeyValuePairs status?: Status object_payload?: Uint8Array @@ -165,22 +165,32 @@ export namespace ObjectDatagram { export function serialize(obj: ObjectDatagram): Uint8Array { const buf = new MutableBytesBuffer(new Uint8Array()) buf.putBytes(ObjectDatagramType.serialize(obj.type)) + buf.putVarInt(obj.track_alias) buf.putVarInt(obj.group_id) - if (obj.object_id) { + if (ObjectDatagramType.hasObjectId(obj.type) && obj.object_id !== undefined) { buf.putVarInt(obj.object_id) } - if (obj.object_payload) { - buf.putVarInt(obj.object_payload.byteLength) - buf.putBytes(obj.object_payload) + if (!ObjectDatagramType.hasDefaultPriority(obj.type) && obj.publisher_priority !== undefined) { + buf.putU8(obj.publisher_priority) + } + if (ObjectDatagramType.hasExtensions(obj.type) && obj.extension_headers) { + const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) + buf.putVarInt(extHeadersBytes.length) + buf.putBytes(extHeadersBytes) + } + if (ObjectDatagramType.hasStatus(obj.type)) { + if (obj.status !== undefined) { + buf.putVarInt(obj.status) + } } else { - buf.putVarInt(0) - buf.putVarInt(obj.status as number) + if (obj.object_payload) { + buf.putBytes(obj.object_payload) + } } return buf.Uint8Array } export function deserialize(reader: ImmutableBytesBuffer): ObjectDatagram { - const type = reader.getNumberVarInt() const alias = reader.getVarInt() const group = reader.getNumberVarInt() @@ -188,13 +198,15 @@ export namespace ObjectDatagram { if (ObjectDatagramType.hasObjectId(type)) { object_id = reader.getNumberVarInt() } - const publisher_priority = reader.getU8() + let publisher_priority: number | undefined + if (!ObjectDatagramType.hasDefaultPriority(type)) { + publisher_priority = reader.getU8() + } let extHeaders: KeyValuePairs | undefined if (ObjectDatagramType.hasExtensions(type)) { - const extHeadersLength = reader.getNumberVarInt() - const extHeadersData = reader.getBytes(extHeadersLength) - - extHeaders = KeyValuePairs.deserialize_with_size(new ImmutableBytesBuffer(extHeadersData), extHeadersLength) + const extHeadersBytesLength = reader.getNumberVarInt() + const extHeadersData = reader.getBytes(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) } let status: Status | undefined let payload: Uint8Array | undefined @@ -277,7 +289,10 @@ export class Objects { subgroup_id = undefined } - const publisher_priority = await r.getU8() + let publisher_priority: number | undefined + if (!SubgroupType.hasDefaultPriority(subgroupType)) { + publisher_priority = await r.getU8() + } const h: SubgroupHeader = { type: subgroupType, @@ -343,10 +358,15 @@ export class TrackReader { if (ObjectDatagramType.hasObjectId(type)) { object_id = await this.stream.getNumberVarInt() } - const publisher_priority = await this.stream.getU8() + let publisher_priority: number | undefined + if (!ObjectDatagramType.hasDefaultPriority(type)) { + publisher_priority = await this.stream.getU8() + } let extHeaders: KeyValuePairs | undefined if (ObjectDatagramType.hasExtensions(type)) { - extHeaders = await KeyValuePairs.deserialize_with_reader(this.stream) + const extHeadersBytesLength = await this.stream.getNumberVarInt() + const extHeadersData = await this.stream.read(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) } let status: Status | undefined let payload: Uint8Array | undefined diff --git a/lib/transport/stream.ts b/lib/transport/stream.ts index 27a2086..19b2fcc 100644 --- a/lib/transport/stream.ts +++ b/lib/transport/stream.ts @@ -1,14 +1,16 @@ import { - ControlMessageType, FetchError, + ControlMessageType, MessageWithType, Publish, - PublishDone, PublishError, PublishNamespace, - PublishNamespaceDone, PublishNamespaceError, - PublishNamespaceOk, PublishOk, Unsubscribe, + PublishDone, PublishNamespace, + PublishNamespaceDone, PublishNamespaceCancel, + PublishOk, Unsubscribe, Fetch, FetchOk, FetchCancel, - Subscribe, SubscribeOk, SubscribeError, + Subscribe, SubscribeOk, SubscribeUpdate, SubscribeNamespace, - SubscribeNamespaceOk, SubscribeNamespaceError, + Namespace, NamespaceDone, TrackStatus, + MaxRequestId, RequestsBlocked, + RequestOk, RequestError, } from "./control" import { debug } from "./utils" import { ImmutableBytesBuffer, ReadableWritableStreamBuffer, Reader, Writer } from "./buffer" @@ -106,12 +108,6 @@ export class Decoder { message: SubscribeOk.deserialize(payload), } break - case ControlMessageType.SubscribeError: - res = { - type: t, - message: SubscribeError.deserialize(payload), - } - break case ControlMessageType.Unsubscribe: res = { type: t, @@ -142,76 +138,88 @@ export class Decoder { message: PublishOk.deserialize(payload), } break - case ControlMessageType.PublishError: + case ControlMessageType.PublishNamespace: res = { type: t, - message: PublishError.deserialize(payload), + message: PublishNamespace.deserialize(payload), } break - case ControlMessageType.PublishNamespace: + case ControlMessageType.PublishNamespaceDone: res = { type: t, - message: PublishNamespace.deserialize(payload), + message: PublishNamespaceDone.deserialize(payload), } break - case ControlMessageType.PublishNamespaceOk: + case ControlMessageType.Fetch: res = { type: t, - message: PublishNamespaceOk.deserialize(payload), + message: Fetch.deserialize(payload), } break - case ControlMessageType.PublishNamespaceDone: + case ControlMessageType.FetchCancel: res = { type: t, - message: PublishNamespaceDone.deserialize(payload), + message: FetchCancel.deserialize(payload), } break - case ControlMessageType.PublishNamespaceError: + case ControlMessageType.FetchOk: res = { type: t, - message: PublishNamespaceError.deserialize(payload), + message: FetchOk.deserialize(payload), } break - case ControlMessageType.Fetch: + case ControlMessageType.SubscribeNamespace: res = { type: t, - message: Fetch.deserialize(payload), + message: SubscribeNamespace.deserialize(payload), } break - case ControlMessageType.FetchCancel: + case ControlMessageType.RequestOk: res = { type: t, - message: FetchCancel.deserialize(payload), + message: RequestOk.deserialize(payload), } break - case ControlMessageType.FetchOk: + case ControlMessageType.RequestError: res = { type: t, - message: FetchOk.deserialize(payload), + message: RequestError.deserialize(payload), } break - case ControlMessageType.FetchError: + case ControlMessageType.PublishNamespaceCancel: res = { type: t, - message: FetchError.deserialize(payload), + message: PublishNamespaceCancel.deserialize(payload), } break - case ControlMessageType.SubscribeNamespace: + case ControlMessageType.Namespace: res = { type: t, - message: SubscribeNamespace.deserialize(payload), + message: Namespace.deserialize(payload), } break - case ControlMessageType.SubscribeNamespaceOk: + case ControlMessageType.NamespaceDone: res = { type: t, - message: SubscribeNamespaceOk.deserialize(payload), + message: NamespaceDone.deserialize(payload), } break - case ControlMessageType.SubscribeNamespaceError: + case ControlMessageType.TrackStatus: res = { type: t, - message: SubscribeNamespaceError.deserialize(payload), + message: TrackStatus.deserialize(payload), + } + break + case ControlMessageType.MaxRequestId: + res = { + type: t, + message: MaxRequestId.deserialize(payload), + } + break + case ControlMessageType.RequestsBlocked: + res = { + type: t, + message: RequestsBlocked.deserialize(payload), } break default: @@ -237,16 +245,10 @@ export class Encoder { return Subscribe.serialize(message as Subscribe) case ControlMessageType.SubscribeOk: return SubscribeOk.serialize(message as SubscribeOk) - case ControlMessageType.SubscribeError: - return SubscribeError.serialize(message as SubscribeError) case ControlMessageType.SubscribeUpdate: return SubscribeUpdate.serialize(message as SubscribeUpdate) case ControlMessageType.SubscribeNamespace: return SubscribeNamespace.serialize(message as SubscribeNamespace) - case ControlMessageType.SubscribeNamespaceOk: - return SubscribeNamespaceOk.serialize(message as SubscribeNamespaceOk) - case ControlMessageType.SubscribeNamespaceError: - return SubscribeNamespaceError.serialize(message as SubscribeNamespaceError) case ControlMessageType.Unsubscribe: return Unsubscribe.serialize(message as Unsubscribe) case ControlMessageType.Publish: @@ -255,14 +257,8 @@ export class Encoder { return PublishDone.serialize(message as PublishDone) case ControlMessageType.PublishOk: return PublishOk.serialize(message as PublishOk) - case ControlMessageType.PublishError: - return PublishError.serialize(message as PublishError) case ControlMessageType.PublishNamespace: return PublishNamespace.serialize(message as PublishNamespace) - case ControlMessageType.PublishNamespaceOk: - return PublishNamespaceOk.serialize(message as PublishNamespaceOk) - case ControlMessageType.PublishNamespaceError: - return PublishNamespaceError.serialize(message as PublishNamespaceError) case ControlMessageType.PublishNamespaceDone: return PublishNamespaceDone.serialize(message as PublishNamespaceDone) case ControlMessageType.Fetch: @@ -271,8 +267,22 @@ export class Encoder { return FetchCancel.serialize(message as FetchCancel) case ControlMessageType.FetchOk: return FetchOk.serialize(message as FetchOk) - case ControlMessageType.FetchError: - return FetchError.serialize(message as FetchError) + case ControlMessageType.RequestOk: + return RequestOk.serialize(message as RequestOk) + case ControlMessageType.RequestError: + return RequestError.serialize(message as RequestError) + case ControlMessageType.PublishNamespaceCancel: + return PublishNamespaceCancel.serialize(message as PublishNamespaceCancel) + case ControlMessageType.Namespace: + return Namespace.serialize(message as Namespace) + case ControlMessageType.NamespaceDone: + return NamespaceDone.serialize(message as NamespaceDone) + case ControlMessageType.TrackStatus: + return TrackStatus.serialize(message as TrackStatus) + case ControlMessageType.MaxRequestId: + return MaxRequestId.serialize(message as MaxRequestId) + case ControlMessageType.RequestsBlocked: + return RequestsBlocked.serialize(message as RequestsBlocked) default: throw new Error(`unknown message kind in encoder`) } @@ -282,4 +292,3 @@ export class Encoder { await this.w.write(payload) } } - diff --git a/lib/transport/subgroup.ts b/lib/transport/subgroup.ts index 4c34026..ae81db7 100644 --- a/lib/transport/subgroup.ts +++ b/lib/transport/subgroup.ts @@ -7,7 +7,7 @@ export interface SubgroupHeader { track_alias: bigint group_id: number subgroup_id?: number - publisher_priority: number + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set } export namespace SubgroupHeader { @@ -19,7 +19,9 @@ export namespace SubgroupHeader { if (SubgroupType.hasExplicitSubgroupId(header.type) && header.subgroup_id !== undefined) { buf.putVarInt(header.subgroup_id) } - buf.putU8(header.publisher_priority) + if (!SubgroupType.hasDefaultPriority(header.type) && header.publisher_priority !== undefined) { + buf.putU8(header.publisher_priority) + } return buf.Uint8Array } } @@ -37,7 +39,9 @@ export namespace SubgroupObject { buf.putVarInt(obj.object_id) if (obj.extension_headers) { - buf.putBytes(KeyValuePairs.serialize(obj.extension_headers)) + const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) + buf.putVarInt(extHeadersBytes.length) + buf.putBytes(extHeadersBytes) } buf.putVarInt(obj.object_payload?.length ?? 0) if (!obj.object_payload) { @@ -49,7 +53,16 @@ export namespace SubgroupObject { } } +// Draft-16: Subgroup header types use bitmask structure 0b00X1XXXX +// Valid ranges: 0x10..0x15, 0x18..0x1D, 0x30..0x35, 0x38..0x3D +// Bit layout: +// bit 0 (0x01) = EXTENSIONS +// bits 1-2 (0x06) = SUBGROUP_ID_MODE (00=zero, 01=first obj, 10=present, 11=RESERVED) +// bit 3 (0x08) = END_OF_GROUP +// bit 4 (0x10) = always set (stream type marker) +// bit 5 (0x20) = DEFAULT_PRIORITY (when set, priority field omitted) export enum SubgroupType { + // Base types (0x10..0x1D) Type0x10 = 0x10, Type0x11 = 0x11, Type0x12 = 0x12, @@ -62,9 +75,28 @@ export enum SubgroupType { Type0x1B = 0x1B, Type0x1C = 0x1C, Type0x1D = 0x1D, + // DEFAULT_PRIORITY types (0x30..0x3D) + Type0x30 = 0x30, + Type0x31 = 0x31, + Type0x32 = 0x32, + Type0x33 = 0x33, + Type0x34 = 0x34, + Type0x35 = 0x35, + Type0x38 = 0x38, + Type0x39 = 0x39, + Type0x3A = 0x3A, + Type0x3B = 0x3B, + Type0x3C = 0x3C, + Type0x3D = 0x3D, } export namespace SubgroupType { + // Bitmask constants + const EXTENSIONS_BIT = 0x01 + const SUBGROUP_ID_MASK = 0x06 + const END_OF_GROUP_BIT = 0x08 + const DEFAULT_PRIORITY_BIT = 0x20 + export function serialize(type: SubgroupType): Uint8Array { const w = new MutableBytesBuffer(new Uint8Array()) w.putVarInt(type) @@ -74,39 +106,27 @@ export namespace SubgroupType { return try_from(reader.getNumberVarInt()) } - // may throw if invalid value if provided + // may throw if invalid value is provided export function try_from(value: number | bigint): SubgroupType { const v = typeof value === "bigint" ? Number(value) : value - switch (v) { - case SubgroupType.Type0x10: - case SubgroupType.Type0x11: - case SubgroupType.Type0x12: - case SubgroupType.Type0x13: - case SubgroupType.Type0x14: - case SubgroupType.Type0x15: - case SubgroupType.Type0x18: - case SubgroupType.Type0x19: - case SubgroupType.Type0x1A: - case SubgroupType.Type0x1B: - case SubgroupType.Type0x1C: - case SubgroupType.Type0x1D: - return v as SubgroupType - default: - throw new Error(`invalid subgroup type: ${v}`) + // Must match form 0b00X1XXXX (bit 4 set) + if ((v & 0x10) === 0) { + throw new Error(`invalid subgroup type: ${v} (bit 4 not set)`) + } + // Must be in ranges 0x10..0x1F or 0x30..0x3F + if (v < 0x10 || (v > 0x1F && v < 0x30) || v > 0x3F) { + throw new Error(`invalid subgroup type: ${v} (out of range)`) } + // SUBGROUP_ID_MODE = 0b11 is reserved + if (((v & SUBGROUP_ID_MASK) >> 1) === 3) { + throw new Error(`invalid subgroup type: ${v} (reserved SUBGROUP_ID_MODE=0b11)`) + } + return v as SubgroupType } export function isSubgroupIdPresent(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x14: - case SubgroupType.Type0x15: - case SubgroupType.Type0x1C: - case SubgroupType.Type0x1D: - return true - default: - return false - } + return ((type & SUBGROUP_ID_MASK) >> 1) === 2 } export function hasExplicitSubgroupId(type: SubgroupType) { @@ -114,47 +134,23 @@ export namespace SubgroupType { } export function isSubgroupIdZero(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x10: - case SubgroupType.Type0x11: - case SubgroupType.Type0x18: - case SubgroupType.Type0x19: - return true - default: - return false - } + return ((type & SUBGROUP_ID_MASK) >> 1) === 0 } export function isSubgroupFirstObjectId(type: SubgroupType) { - return !(hasExplicitSubgroupId(type) || isSubgroupIdZero(type)) + return ((type & SUBGROUP_ID_MASK) >> 1) === 1 } export function isExtensionPresent(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x11: - case SubgroupType.Type0x13: - case SubgroupType.Type0x15: - case SubgroupType.Type0x19: - case SubgroupType.Type0x1B: - case SubgroupType.Type0x1D: - return true - default: - return false - } + return (type & EXTENSIONS_BIT) !== 0 } export function contains_end_of_group(type: SubgroupType) { - switch (type) { - case SubgroupType.Type0x18: - case SubgroupType.Type0x19: - case SubgroupType.Type0x1A: - case SubgroupType.Type0x1B: - case SubgroupType.Type0x1C: - case SubgroupType.Type0x1D: - return true - default: - return false - } + return (type & END_OF_GROUP_BIT) !== 0 + } + + export function hasDefaultPriority(type: SubgroupType) { + return (type & DEFAULT_PRIORITY_BIT) !== 0 } } @@ -188,7 +184,9 @@ export class SubgroupReader { let extHeaders: KeyValuePairs | undefined if (SubgroupType.isExtensionPresent(this.header.type)) { - extHeaders = await KeyValuePairs.deserialize_with_reader(this.stream) + const extHeadersBytesLength = await this.stream.getNumberVarInt() + const extHeadersData = await this.stream.read(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) } console.log("subgroup header", object_id, extHeaders, this.stream) From eb6b00748771fb14acf98deadc2d547acf25cce2 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 11 Mar 2026 22:21:30 +0530 Subject: [PATCH 2/3] fix: server and client version negotiation --- lib/transport/client.ts | 9 +++---- lib/transport/publisher.ts | 39 ++++++++++++++--------------- lib/transport/subscriber.ts | 49 +++++++++++++++++++++++-------------- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/lib/transport/client.ts b/lib/transport/client.ts index b377841..0440cbb 100644 --- a/lib/transport/client.ts +++ b/lib/transport/client.ts @@ -40,7 +40,6 @@ export class Client { const buffer = new ReadableWritableStreamBuffer(stream.readable, stream.writable) const msg: Control.ClientSetup = { - versions: [Control.Version.DRAFT_14], params: new Map(), } const serialized = Control.ClientSetup.serialize(msg) @@ -49,10 +48,10 @@ export class Client { // Receive the setup message. // TODO verify the SETUP response. const server = await this.readServerSetup(buffer) - - if (server.version != Control.Version.DRAFT_14) { - throw new Error(`unsupported server version: ${server.version}`) - } + // + // if (server.version != Control.Version.DRAFT_14) { + // throw new Error(`unsupported server version: ${server.version}`) + // } const control = new Stream.ControlStream(buffer) const objects = new Objects(quic) diff --git a/lib/transport/publisher.ts b/lib/transport/publisher.ts index 32a6c33..509d2cb 100644 --- a/lib/transport/publisher.ts +++ b/lib/transport/publisher.ts @@ -3,6 +3,7 @@ import { ControlStream } from "./stream" import { Queue, Watch } from "../common/async" import { Objects, TrackWriter, ObjectDatagramType } from "./objects" import { SubgroupType, SubgroupWriter } from "./subgroup" +import { Parameters, ParameterType } from "./base_data" export class Publisher { // Used to send control messages @@ -62,21 +63,21 @@ export class Publisher { case Control.ControlMessageType.Unsubscribe: this.recvUnsubscribe(message) break; - case Control.ControlMessageType.PublishNamespaceOk: - this.recvPublishNamespaceOk(message) + case Control.ControlMessageType.RequestOk: + this.recvRequestOk(message) break; - case Control.ControlMessageType.PublishNamespaceError: - this.recvPublishNamespaceError(message) + case Control.ControlMessageType.RequestError: + this.recvRequestError(message) break; default: throw new Error(`unknown control message`) // impossible } } - recvPublishNamespaceOk(msg: Control.PublishNamespaceOk) { + recvRequestOk(msg: Control.RequestOk) { const namespace = this.#waitingPublishNamespaceRequests.get(msg.id) if (!namespace) { - throw new Error(`publish namespace OK for unknown announce: ${msg.id}`) + throw new Error(`request OK for unknown request: ${msg.id}`) } const publishNamespaceSend = this.#publishedNamespaces.get(namespace) if (!publishNamespaceSend) { @@ -87,15 +88,15 @@ export class Publisher { console.log("published namespace:", namespace) } - recvPublishNamespaceError(msg: Control.PublishNamespaceError) { + recvRequestError(msg: Control.RequestError) { const namespace = this.#waitingPublishNamespaceRequests.get(msg.id) if (!namespace) { - throw new Error(`publish namespace error for unknown announce: ${msg.id}`) + throw new Error(`request error for unknown request: ${msg.id}`) } const publishNamespaceSend = this.#publishedNamespaces.get(namespace) if (!publishNamespaceSend) { // TODO debug this - console.warn(`publish namespace error for unknown announce: ${namespace}`) + console.warn(`request error for unknown namespace: ${namespace}`) return } @@ -114,10 +115,11 @@ export class Publisher { await this.#subscribeQueue.push(subscribe) } catch (e: any) { await this.#control.send({ - type: Control.ControlMessageType.SubscribeError, + type: Control.ControlMessageType.RequestError, message: { id: msg.id, code: 0n, + retry_interval: 0n, reason: e.message, } }) @@ -187,7 +189,7 @@ export class PublishNamespaceSend { onError(code: bigint, reason: string) { if (this.closed()) return - const err = new Error(`PUBLISH_NAMESPACE_ERROR (${code})` + reason ? `: ${reason}` : "") + const err = new Error(`REQUEST_ERROR (${code})${reason ? `: ${reason}` : ""}`) this.#state.update(err) } } @@ -197,8 +199,7 @@ export class SubscribeRecv { #objects: Objects #id: bigint #trackAlias: bigint // Publisher-specified in draft-14 - #subscriberPriority: number - groupOrder: Control.GroupOrder + params: Parameters; readonly namespace: string[] readonly track: string @@ -213,8 +214,7 @@ export class SubscribeRecv { this.#trackAlias = trackAlias this.namespace = msg.namespace this.track = msg.name - this.#subscriberPriority = msg.subscriber_priority - this.groupOrder = msg.group_order + this.params = msg.params } // Acknowledge the subscription as valid. @@ -230,11 +230,8 @@ export class SubscribeRecv { type: Control.ControlMessageType.SubscribeOk, message: { id: this.#id, - expires: 0n, - group_order: this.groupOrder, track_alias: this.#trackAlias, - content_exists: 0, - params: new Map(), + params: this.params, } }) } @@ -247,8 +244,8 @@ export class SubscribeRecv { if (!acked) { return this.#control.send({ - type: Control.ControlMessageType.SubscribeError, - message: { id: this.#id, code, reason } + type: Control.ControlMessageType.RequestError, + message: { id: this.#id, code, retry_interval: 0n, reason } }) } if (unsubscribe) { diff --git a/lib/transport/subscriber.ts b/lib/transport/subscriber.ts index ac1d8ab..055d196 100644 --- a/lib/transport/subscriber.ts +++ b/lib/transport/subscriber.ts @@ -5,6 +5,7 @@ import type { TrackReader } from "./objects" import { debug } from "./utils" import { ControlStream } from "./stream" import { SubgroupReader } from "./subgroup" +import { ParameterType } from "./base_data" export interface TrackInfo { track_alias: bigint @@ -50,8 +51,8 @@ export class Subscriber { case Control.ControlMessageType.SubscribeOk: this.recvSubscribeOk(message) break - case Control.ControlMessageType.SubscribeError: - await this.recvSubscribeError(message) + case Control.ControlMessageType.RequestError: + await this.recvRequestError(message) break case Control.ControlMessageType.PublishDone: await this.recvPublishDone(message) @@ -67,8 +68,8 @@ export class Subscriber { } await this.#control.send({ - type: Control.ControlMessageType.PublishNamespaceOk, - message: { id: msg.id } + type: Control.ControlMessageType.RequestOk, + message: { id: msg.id, parameters: new Map() } }) const publishNamespace = new PublishNamespaceRecv(this.#control, msg.namespace, msg.id) @@ -89,12 +90,17 @@ export class Subscriber { message: { id, namespace, + subscribe_options: Control.SubscribeOptions.BOTH, } } await this.#control.send(msg) } - async subscribe(namespace: string[], track: string) { + async subscribe(namespace: string[], track: string, opts?: { + forward?: number, + subscriber_priority?: number, + group_order?: Control.GroupOrder, + }) { const id = this.#control.nextRequestId() const subscribe = new SubscribeSend(this.#control, id, namespace, track) @@ -102,17 +108,24 @@ export class Subscriber { this.#trackToIDMap.set(track, id) + const params = new Map() + if (opts?.forward !== undefined) { + params.set(BigInt(ParameterType.FORWARD), BigInt(opts.forward)) + } + if (opts?.subscriber_priority !== undefined) { + params.set(BigInt(ParameterType.SUBSCRIBER_PRIORITY), BigInt(opts.subscriber_priority)) + } + if (opts?.group_order !== undefined) { + params.set(BigInt(ParameterType.GROUP_ORDER), BigInt(opts.group_order)) + } + const subscription_req: Control.MessageWithType = { type: Control.ControlMessageType.Subscribe, message: { id, namespace, name: track, - subscriber_priority: 127, // default to mid value, see: https://github.com/moq-wg/moq-transport/issues/504 - group_order: Control.GroupOrder.Publisher, - filter_type: Control.FilterType.NextGroupStart, - forward: 1, // always forward - params: new Map(), + params, } } @@ -160,10 +173,10 @@ export class Subscriber { subscribe.onOk(msg.track_alias) } - async recvSubscribeError(msg: Control.SubscribeError) { + async recvRequestError(msg: Control.RequestError) { const subscribe = this.#subscribe.get(msg.id) if (!subscribe) { - throw new Error(`subscribe error for unknown id: ${msg.id}`) + throw new Error(`request error for unknown id: ${msg.id}`) } await subscribe.onError(msg.code, msg.reason) @@ -226,8 +239,8 @@ export class PublishNamespaceRecv { // Send the control message. return this.#control.send({ - type: Control.ControlMessageType.PublishNamespaceOk, - message: { id: this.#id } + type: Control.ControlMessageType.RequestOk, + message: { id: this.#id, parameters: new Map() } }) } @@ -236,8 +249,8 @@ export class PublishNamespaceRecv { this.#state = "closed" return this.#control.send({ - type: Control.ControlMessageType.PublishNamespaceError, - message: { id: this.#id, code, reason } + type: Control.ControlMessageType.RequestError, + message: { id: this.#id, code, retry_interval: 0n, reason } }) } } @@ -274,7 +287,7 @@ export class SubscribeSend { this.#trackAlias = trackAlias } - // FIXME(itzmanish): implement correctly + // FIXME(itzmanish): implement correctly async onDone(code: bigint, streamCount: bigint, reason: string) { throw new Error(`TODO onDone`) } @@ -288,7 +301,7 @@ export class SubscribeSend { reason = `: ${reason}` } - const err = new Error(`SUBSCRIBE_ERROR (${code})${reason}`) + const err = new Error(`REQUEST_ERROR (${code})${reason}`) return await this.#data.abort(err) } From 4620a4aee6f7eedf6cb53e2914737a5790a2cea2 Mon Sep 17 00:00:00 2001 From: Manish Date: Wed, 11 Mar 2026 22:27:28 +0530 Subject: [PATCH 3/3] fix: npm run fmt --- lib/common/tsconfig.json | 6 +- lib/contribute/broadcast.ts | 14 +- lib/contribute/container.ts | 2 +- lib/contribute/track.ts | 2 +- lib/contribute/tsconfig.json | 11 +- lib/media/tsconfig.json | 10 +- lib/moq-publisher/index.ts | 2 +- lib/package.json | 2 +- lib/playback/index.ts | 2 +- lib/playback/worker/index.ts | 2 +- lib/playback/worker/video.ts | 2 +- lib/publish/index.ts | 2 +- lib/transport/base_data.ts | 22 +- lib/transport/buffer.ts | 1101 ++++++++--------- lib/transport/client.ts | 8 +- lib/transport/connection.ts | 4 +- lib/transport/control/client_setup.ts | 2 +- lib/transport/control/fetch.ts | 9 +- lib/transport/control/fetch_cancel.ts | 37 +- lib/transport/control/fetch_ok.ts | 2 +- lib/transport/control/go_away.ts | 36 +- lib/transport/control/index.ts | 112 +- lib/transport/control/max_request_id.ts | 35 +- lib/transport/control/namespace.ts | 1 - lib/transport/control/namespace_done.ts | 1 - lib/transport/control/publish.ts | 2 +- lib/transport/control/publish_done.ts | 3 +- lib/transport/control/publish_namespace.ts | 3 +- .../control/publish_namespace_cancel.ts | 3 +- lib/transport/control/publish_ok.ts | 4 +- lib/transport/control/request_error.ts | 6 +- lib/transport/control/request_ok.ts | 46 +- lib/transport/control/requests_blocked.ts | 1 - lib/transport/control/server_setup.ts | 2 +- lib/transport/control/setup_parameters.ts | 11 +- lib/transport/control/subscribe.ts | 4 +- lib/transport/control/subscribe_namespace.ts | 6 +- lib/transport/control/subscribe_ok.ts | 1 - lib/transport/control/subscribe_update.ts | 5 +- lib/transport/control/track_status.ts | 1 - lib/transport/control/unsubscribe.ts | 35 +- lib/transport/fetch.ts | 28 +- lib/transport/objects.ts | 27 +- lib/transport/publisher.ts | 34 +- lib/transport/stream.ts | 34 +- lib/transport/subgroup.ts | 369 +++--- lib/transport/subscriber.ts | 26 +- lib/transport/tsconfig.json | 6 +- lib/transport/utils.ts | 6 +- lib/tsconfig.json | 12 +- lib/video-moq/index.ts | 38 +- 51 files changed, 1071 insertions(+), 1069 deletions(-) diff --git a/lib/common/tsconfig.json b/lib/common/tsconfig.json index 47e2883..20e0199 100644 --- a/lib/common/tsconfig.json +++ b/lib/common/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "composite": true }, - "include": [ - "." - ] -} \ No newline at end of file + "include": ["."] +} diff --git a/lib/contribute/broadcast.ts b/lib/contribute/broadcast.ts index 43f140c..887c8af 100644 --- a/lib/contribute/broadcast.ts +++ b/lib/contribute/broadcast.ts @@ -46,7 +46,7 @@ export class Broadcast { const settings = media.getSettings() if (media.kind === "audio") { - const audioContext = new AudioContext(); + const audioContext = new AudioContext() audioContext.createMediaStreamSource(new MediaStream([media])) const sampleRate = audioContext.sampleRate Object.assign(settings, { @@ -121,7 +121,7 @@ export class Broadcast { console.log("[Broadcast] #run loop started") await this.connection.publish_namespace(this.namespace) - for (; ;) { + for (;;) { const subscriber = await this.connection.subscribed() if (!subscriber) break @@ -164,7 +164,7 @@ export class Broadcast { const bytes = Catalog.encode(this.catalog) await subscriber.ack() - await sleep(500); + await sleep(500) const stream = await subscriber.subgroup({ group: 0, subgroup: 0 }) await stream.write({ object_id: 0, object_payload: bytes }) @@ -176,7 +176,7 @@ export class Broadcast { if (!track) throw new Error(`no track with name ${subscriber.track}`) await subscriber.ack() - await sleep(500); + await sleep(500) const init = await track.init() @@ -193,11 +193,11 @@ export class Broadcast { await subscriber.ack() // NOTE(itzmanish): hack to make sure subscribe ok reaches before the segement object - await sleep(500); + await sleep(500) const segments = track.segments().getReader() - for (; ;) { + for (;;) { const { value: segment, done } = await segments.read() if (done) break @@ -221,7 +221,7 @@ export class Broadcast { // Pipe the segment to the stream. const chunks = segment.chunks().getReader() - for (; ;) { + for (;;) { const { value, done } = await chunks.read() if (done) break diff --git a/lib/contribute/container.ts b/lib/contribute/container.ts index b82d4aa..056e59f 100644 --- a/lib/contribute/container.ts +++ b/lib/contribute/container.ts @@ -125,7 +125,7 @@ export class Container { // Moof and mdat atoms are written in pairs. // TODO remove the moof/mdat from the Box to reclaim memory once everything works - for (; ;) { + for (;;) { const moof = this.#mp4.moofs.shift() const mdat = this.#mp4.mdats.shift() diff --git a/lib/contribute/track.ts b/lib/contribute/track.ts index e184ea7..c5b9daf 100644 --- a/lib/contribute/track.ts +++ b/lib/contribute/track.ts @@ -141,7 +141,7 @@ export class Track { return new ReadableStream({ pull: async (controller) => { - for (; ;) { + for (;;) { let index = pos - this.#offset if (index < 0) index = 0 diff --git a/lib/contribute/tsconfig.json b/lib/contribute/tsconfig.json index e198b77..70899a7 100644 --- a/lib/contribute/tsconfig.json +++ b/lib/contribute/tsconfig.json @@ -1,13 +1,8 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "include": ["."], "compilerOptions": { - "types": [ - "dom-mediacapture-transform", - "dom-webcodecs" - ] + "types": ["dom-mediacapture-transform", "dom-webcodecs"] }, "references": [ { @@ -20,4 +15,4 @@ "path": "../media" } ] -} \ No newline at end of file +} diff --git a/lib/media/tsconfig.json b/lib/media/tsconfig.json index 1e2adde..2e4ee28 100644 --- a/lib/media/tsconfig.json +++ b/lib/media/tsconfig.json @@ -1,12 +1,8 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "include": ["."], "compilerOptions": { - "types": [ - "mp4box" - ] + "types": ["mp4box"] }, "references": [ { @@ -16,4 +12,4 @@ "path": "../common" } ] -} \ No newline at end of file +} diff --git a/lib/moq-publisher/index.ts b/lib/moq-publisher/index.ts index f976f1d..9cfa3c4 100644 --- a/lib/moq-publisher/index.ts +++ b/lib/moq-publisher/index.ts @@ -194,4 +194,4 @@ export class PublisherMoq extends HTMLElement { } customElements.define("publisher-moq", PublisherMoq) -export default PublisherMoq \ No newline at end of file +export default PublisherMoq diff --git a/lib/package.json b/lib/package.json index 4a5f81d..2f6b02e 100644 --- a/lib/package.json +++ b/lib/package.json @@ -83,4 +83,4 @@ "type": "git", "url": "https://github.com/englishm/moq-js" } -} \ No newline at end of file +} diff --git a/lib/playback/index.ts b/lib/playback/index.ts index d468b96..bd1cd12 100644 --- a/lib/playback/index.ts +++ b/lib/playback/index.ts @@ -154,7 +154,7 @@ export default class Player extends EventTarget { try { console.log("starting segment data loop") - for (; ;) { + for (;;) { console.log("waiting for segment data") const segment = await Promise.race([sub.data(), this.#running]) if (!segment) continue diff --git a/lib/playback/worker/index.ts b/lib/playback/worker/index.ts index 4a9d556..651ee49 100644 --- a/lib/playback/worker/index.ts +++ b/lib/playback/worker/index.ts @@ -87,7 +87,7 @@ class Worker { segments.releaseLock() // Read each chunk, decoding the MP4 frames and adding them to the queue. - for (; ;) { + for (;;) { const chunk = await reader.read() if (!chunk) { break diff --git a/lib/playback/worker/video.ts b/lib/playback/worker/video.ts index a8691d6..e841a51 100644 --- a/lib/playback/worker/video.ts +++ b/lib/playback/worker/video.ts @@ -57,7 +57,7 @@ export class Renderer { async #run() { const reader = this.#timeline.frames.pipeThrough(this.#queue).getReader() - for (; ;) { + for (;;) { const { value: frame, done } = await reader.read() if (this.#paused) continue if (done) break diff --git a/lib/publish/index.ts b/lib/publish/index.ts index ac6b6a2..08e1e21 100644 --- a/lib/publish/index.ts +++ b/lib/publish/index.ts @@ -51,4 +51,4 @@ export class PublisherApi { await this.connection.closed() } } -} \ No newline at end of file +} diff --git a/lib/transport/base_data.ts b/lib/transport/base_data.ts index 43ff993..c7cd0ed 100644 --- a/lib/transport/base_data.ts +++ b/lib/transport/base_data.ts @@ -8,7 +8,7 @@ export namespace Tuple { export function serialize(tuple: Tuple): Uint8Array { const buf = new MutableBytesBuffer(new Uint8Array()) buf.putVarInt(tuple.length) - tuple.forEach(field => { + tuple.forEach((field) => { const serialized = TupleField.serialize(field) buf.putBytes(serialized) }) @@ -64,7 +64,6 @@ export namespace Location { } } - // Draft-16: Key-Value-Pairs use delta-encoded types (Section 1.4.2) // Delta Type is delta from previous type (or 0 if first) // Type even => value is varint (no length prefix) @@ -72,7 +71,6 @@ export namespace Location { export type KeyValuePairs = Map export type Parameters = KeyValuePairs - export namespace KeyValuePairs { export function valueIsVarInt(key: bigint): boolean { return (key & 1n) === 0n @@ -179,7 +177,6 @@ export namespace KeyValuePairs { } } - export namespace Parameters { export function valueIsVarInt(key: bigint): boolean { return KeyValuePairs.valueIsVarInt(key) @@ -206,7 +203,6 @@ export namespace Parameters { } } - // Draft-16: Reason Phrase structure (Section 1.4.3) // Max length is 1024 bytes export const REASON_PHRASE_MAX_LENGTH = 1024 @@ -244,7 +240,6 @@ export namespace ReasonPhrase { } } - // Draft-16: Parameter Type constants (Section 13.2, Table 8) export enum ParameterType { DELIVERY_TIMEOUT = 0x02, @@ -258,20 +253,18 @@ export enum ParameterType { NEW_GROUP_REQUEST = 0x32, } - // Draft-16: Extension Header Type constants (Section 13.3, Table 9) export enum ExtensionHeaderType { DELIVERY_TIMEOUT = 0x02, MAX_CACHE_DURATION = 0x04, - IMMUTABLE_EXTENSIONS = 0x0B, - DEFAULT_PUBLISHER_PRIORITY = 0x0E, + IMMUTABLE_EXTENSIONS = 0x0b, + DEFAULT_PUBLISHER_PRIORITY = 0x0e, DEFAULT_PUBLISHER_GROUP_ORDER = 0x22, DYNAMIC_GROUPS = 0x30, - PRIOR_GROUP_ID_GAP = 0x3C, - PRIOR_OBJECT_ID_GAP = 0x3E, + PRIOR_GROUP_ID_GAP = 0x3c, + PRIOR_OBJECT_ID_GAP = 0x3e, } - // Draft-16: Session Termination Error Codes (Section 13.4.1, Table 10) export enum SessionTerminationError { NO_ERROR = 0x0, @@ -294,10 +287,9 @@ export enum SessionTerminationError { UNKNOWN_AUTH_TOKEN_ALIAS = 0x17, EXPIRED_AUTH_TOKEN = 0x18, INVALID_AUTHORITY = 0x19, - MALFORMED_AUTHORITY = 0x1A, + MALFORMED_AUTHORITY = 0x1a, } - // Draft-16: REQUEST_ERROR Codes (Section 13.4.2, Table 11) export enum RequestErrorCode { INTERNAL_ERROR = 0x0, @@ -315,7 +307,6 @@ export enum RequestErrorCode { INVALID_JOINING_REQUEST_ID = 0x32, } - // Draft-16: PUBLISH_DONE Codes (Section 13.4.3, Table 12) export enum PublishDoneCode { INTERNAL_ERROR = 0x0, @@ -329,7 +320,6 @@ export enum PublishDoneCode { MALFORMED_TRACK = 0x12, } - // Draft-16: Data Stream Reset Error Codes (Section 13.4.4, Table 13) export enum DataStreamResetCode { INTERNAL_ERROR = 0x0, diff --git a/lib/transport/buffer.ts b/lib/transport/buffer.ts index fa57b72..b2046e0 100644 --- a/lib/transport/buffer.ts +++ b/lib/transport/buffer.ts @@ -5,577 +5,572 @@ const MAX_U53 = Number.MAX_SAFE_INTEGER const MAX_U62: bigint = 2n ** 62n - 1n // 0-4611686018427387903 (62 bits) export interface Reader { - byteLength: number - waitForBytes(len: number): Promise - read(len: number): Promise - done(): Promise - close(): Promise - release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] - - getU8(): Promise - getU16(): Promise - getNumberVarInt(): Promise - getVarInt(): Promise - getVarBytes(): Promise - getUtf8String(): Promise + byteLength: number + waitForBytes(len: number): Promise + read(len: number): Promise + done(): Promise + close(): Promise + release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] + + getU8(): Promise + getU16(): Promise + getNumberVarInt(): Promise + getVarInt(): Promise + getVarBytes(): Promise + getUtf8String(): Promise } export interface Writer { - write(data: Uint8Array): Promise - flush(): Promise - clear(): void - close(): Promise - release(): [Uint8Array, WritableStream] | [Uint8Array, ReadableStream] - - putU8(v: number): void - putU16(v: number): void - putVarInt(v: number | bigint): void - putUtf8String(v: string): void + write(data: Uint8Array): Promise + flush(): Promise + clear(): void + close(): Promise + release(): [Uint8Array, WritableStream] | [Uint8Array, ReadableStream] + + putU8(v: number): void + putU16(v: number): void + putVarInt(v: number | bigint): void + putUtf8String(v: string): void } export class ImmutableBytesBuffer { - protected buffer: Uint8Array - protected offset = 0 - view: DataView - - constructor(buffer: Uint8Array) { - this.buffer = buffer - this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.length) - } - - get length(): number { - return this.buffer.length - } - - get remaining(): number { - return this.length - this.offset - } - - get Uint8Array(): Uint8Array { - return this.buffer.slice(0, this.offset) - } - - get firstByteValue(): number { - return this.buffer[this.offset] - } - - getRemainingBuffer(): Uint8Array { - return this.buffer.subarray(this.offset) - } - - getU8(): number { - if (this.remaining < 1) throw new Error("not enough bytes") - const val = this.view.getUint8(this.offset) - this.offset += 1 - return val - } - - getU16(): number { - if (this.remaining < 2) throw new Error("not enough bytes") - const val = this.view.getUint16(this.offset) - this.offset += 2 - return val - } - - getU32(): number { - if (this.remaining < 4) throw new Error("not enough bytes") - const val = this.view.getUint32(this.offset) - this.offset += 4 - return val - } - - getU64(): bigint { - if (this.remaining < 8) throw new Error("not enough bytes") - const val = this.view.getBigUint64(this.offset) - this.offset += 8 - return val - } - - getVarInt(): bigint { - if (this.remaining < 1) throw new Error("not enough bytes") - - // Read first byte to determine length - const first = this.view.getUint8(this.offset) - const prefix = (first & 0b11000000) >> 6 // Top 2 bits indicate length - - if (prefix === 0) { - // 1 byte: 00xxxxxx - this.offset += 1 - return BigInt(first & 0x3f) - } else if (prefix === 1) { - // 2 bytes: 01xxxxxx xxxxxxxx - if (this.remaining < 2) throw new Error("not enough bytes") - const val = this.view.getUint16(this.offset) - this.offset += 2 - return BigInt(val & 0x3fff) - } else if (prefix === 2) { - // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - if (this.remaining < 4) throw new Error("not enough bytes") - const val = this.view.getUint32(this.offset) - this.offset += 4 - return BigInt(val & 0x3fffffff) - } else { - // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - if (this.remaining < 8) throw new Error("not enough bytes") - const val = this.view.getBigUint64(this.offset) - this.offset += 8 - return val & 0x3fffffffffffffffn - } - } - - getNumberVarInt(): number { - const val = this.getVarInt() - if (val > Number.MAX_SAFE_INTEGER) throw new Error("varint too large") - return Number(val) - } - - getBytes(len: number): Uint8Array { - if (this.remaining < len) throw new Error("not enough bytes") - const val = this.buffer.slice(this.offset, this.offset + len) - this.offset += len - return val - } - - getVarBytes(): Uint8Array { - const len = this.getNumberVarInt() - return this.getBytes(len) - } - - getUtf8String(): string { - const len = this.getNumberVarInt() - const val = this.getBytes(len) - return new TextDecoder().decode(val) - } + protected buffer: Uint8Array + protected offset = 0 + view: DataView + + constructor(buffer: Uint8Array) { + this.buffer = buffer + this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.length) + } + + get length(): number { + return this.buffer.length + } + + get remaining(): number { + return this.length - this.offset + } + + get Uint8Array(): Uint8Array { + return this.buffer.slice(0, this.offset) + } + + get firstByteValue(): number { + return this.buffer[this.offset] + } + + getRemainingBuffer(): Uint8Array { + return this.buffer.subarray(this.offset) + } + + getU8(): number { + if (this.remaining < 1) throw new Error("not enough bytes") + const val = this.view.getUint8(this.offset) + this.offset += 1 + return val + } + + getU16(): number { + if (this.remaining < 2) throw new Error("not enough bytes") + const val = this.view.getUint16(this.offset) + this.offset += 2 + return val + } + + getU32(): number { + if (this.remaining < 4) throw new Error("not enough bytes") + const val = this.view.getUint32(this.offset) + this.offset += 4 + return val + } + + getU64(): bigint { + if (this.remaining < 8) throw new Error("not enough bytes") + const val = this.view.getBigUint64(this.offset) + this.offset += 8 + return val + } + + getVarInt(): bigint { + if (this.remaining < 1) throw new Error("not enough bytes") + + // Read first byte to determine length + const first = this.view.getUint8(this.offset) + const prefix = (first & 0b11000000) >> 6 // Top 2 bits indicate length + + if (prefix === 0) { + // 1 byte: 00xxxxxx + this.offset += 1 + return BigInt(first & 0x3f) + } else if (prefix === 1) { + // 2 bytes: 01xxxxxx xxxxxxxx + if (this.remaining < 2) throw new Error("not enough bytes") + const val = this.view.getUint16(this.offset) + this.offset += 2 + return BigInt(val & 0x3fff) + } else if (prefix === 2) { + // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + if (this.remaining < 4) throw new Error("not enough bytes") + const val = this.view.getUint32(this.offset) + this.offset += 4 + return BigInt(val & 0x3fffffff) + } else { + // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + if (this.remaining < 8) throw new Error("not enough bytes") + const val = this.view.getBigUint64(this.offset) + this.offset += 8 + return val & 0x3fffffffffffffffn + } + } + + getNumberVarInt(): number { + const val = this.getVarInt() + if (val > Number.MAX_SAFE_INTEGER) throw new Error("varint too large") + return Number(val) + } + + getBytes(len: number): Uint8Array { + if (this.remaining < len) throw new Error("not enough bytes") + const val = this.buffer.slice(this.offset, this.offset + len) + this.offset += len + return val + } + + getVarBytes(): Uint8Array { + const len = this.getNumberVarInt() + return this.getBytes(len) + } + + getUtf8String(): string { + const len = this.getNumberVarInt() + const val = this.getBytes(len) + return new TextDecoder().decode(val) + } } export class MutableBytesBuffer { - offset = 0 - buffer: Uint8Array - view: DataView - constructor(buffer: Uint8Array) { - this.buffer = buffer - this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) - } - - get length(): number { - return this.offset - } - - get byteLength(): number { - // NOTE(itzmanish): since we are working with u8 buffer, byteLength is same as length - return this.length - } - - get Uint8Array(): Uint8Array { - return this.buffer.subarray(0, this.offset) - } - - enoughSpaceAvailable(len: number): void { - const required = this.offset + len - if (required < this.buffer.length) { - return - } - - const newBuffer = new Uint8Array(nextPow2(required)) - newBuffer.set(this.buffer.subarray(0, this.offset)) - this.buffer = newBuffer - this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength) - } - - putU8(v: number) { - this.enoughSpaceAvailable(1) - this.view.setUint8(this.offset, v) - this.offset += 1 - } - - putU16(v: number) { - this.enoughSpaceAvailable(2) - this.view.setUint16(this.offset, v) - this.offset += 2 - } - - putU32(v: number) { - this.enoughSpaceAvailable(4) - this.view.setUint32(this.offset, v) - this.offset += 4 - } - - putU64(v: bigint) { - this.enoughSpaceAvailable(8) - this.view.setBigUint64(this.offset, v) - this.offset += 8 - } - - putVarInt(v: number | bigint) { - const value = typeof v === "number" ? BigInt(v) : v - if (value < 0) throw new Error("underflow, value is negative: " + v) - - let length: number - let prefix: number - - if (value < MAX_U6) { - // 1 byte: 00xxxxxx - length = 1 - prefix = 0x00 - } else if (value < MAX_U14) { - // 2 bytes: 01xxxxxx xxxxxxxx - length = 2 - prefix = 0x40 - } else if (value < MAX_U30) { - // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - length = 4 - prefix = 0x80 - } else if (value <= MAX_U62) { - // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx - length = 8 - prefix = 0xc0 - } else { - throw new Error("overflow, value larger than 62-bits: " + v) - } - - this.enoughSpaceAvailable(length) - - // Write first byte with prefix - const shift = BigInt((length - 1) * 8) - this.putU8(Number((value >> shift) | BigInt(prefix))) - - // Write remaining bytes - for (let i = length - 2; i >= 0; i--) { - this.putU8(Number((value >> BigInt(i * 8)) & 0xffn)) - } - } - - putBytes(v: Uint8Array) { - this.enoughSpaceAvailable(v.length) - this.buffer.set(v, this.offset) - this.offset += v.length - } - - putUtf8String(v: string) { - const bytes = new TextEncoder().encode(v) - this.putLengthPrefixedByteArray(bytes.length, bytes) - } - - putLengthPrefixedByteArray(len: number, v: Uint8Array) { - this.putVarInt(len) - this.putBytes(v) - } - + offset = 0 + buffer: Uint8Array + view: DataView + constructor(buffer: Uint8Array) { + this.buffer = buffer + this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) + } + + get length(): number { + return this.offset + } + + get byteLength(): number { + // NOTE(itzmanish): since we are working with u8 buffer, byteLength is same as length + return this.length + } + + get Uint8Array(): Uint8Array { + return this.buffer.subarray(0, this.offset) + } + + enoughSpaceAvailable(len: number): void { + const required = this.offset + len + if (required < this.buffer.length) { + return + } + + const newBuffer = new Uint8Array(nextPow2(required)) + newBuffer.set(this.buffer.subarray(0, this.offset)) + this.buffer = newBuffer + this.view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength) + } + + putU8(v: number) { + this.enoughSpaceAvailable(1) + this.view.setUint8(this.offset, v) + this.offset += 1 + } + + putU16(v: number) { + this.enoughSpaceAvailable(2) + this.view.setUint16(this.offset, v) + this.offset += 2 + } + + putU32(v: number) { + this.enoughSpaceAvailable(4) + this.view.setUint32(this.offset, v) + this.offset += 4 + } + + putU64(v: bigint) { + this.enoughSpaceAvailable(8) + this.view.setBigUint64(this.offset, v) + this.offset += 8 + } + + putVarInt(v: number | bigint) { + const value = typeof v === "number" ? BigInt(v) : v + if (value < 0) throw new Error("underflow, value is negative: " + v) + + let length: number + let prefix: number + + if (value < MAX_U6) { + // 1 byte: 00xxxxxx + length = 1 + prefix = 0x00 + } else if (value < MAX_U14) { + // 2 bytes: 01xxxxxx xxxxxxxx + length = 2 + prefix = 0x40 + } else if (value < MAX_U30) { + // 4 bytes: 10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + length = 4 + prefix = 0x80 + } else if (value <= MAX_U62) { + // 8 bytes: 11xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx + length = 8 + prefix = 0xc0 + } else { + throw new Error("overflow, value larger than 62-bits: " + v) + } + + this.enoughSpaceAvailable(length) + + // Write first byte with prefix + const shift = BigInt((length - 1) * 8) + this.putU8(Number((value >> shift) | BigInt(prefix))) + + // Write remaining bytes + for (let i = length - 2; i >= 0; i--) { + this.putU8(Number((value >> BigInt(i * 8)) & 0xffn)) + } + } + + putBytes(v: Uint8Array) { + this.enoughSpaceAvailable(v.length) + this.buffer.set(v, this.offset) + this.offset += v.length + } + + putUtf8String(v: string) { + const bytes = new TextEncoder().encode(v) + this.putLengthPrefixedByteArray(bytes.length, bytes) + } + + putLengthPrefixedByteArray(len: number, v: Uint8Array) { + this.putVarInt(len) + this.putBytes(v) + } } export class ReadableStreamBuffer implements Reader { - protected buffer: Uint8Array - protected reader: ReadableStreamDefaultReader - protected readableStream: ReadableStream - - constructor(reader: ReadableStream, buffer?: Uint8Array) { - this.buffer = buffer ?? new Uint8Array() - this.reader = reader.getReader() - this.readableStream = reader - } - - waitForBytes(len: number): Promise { - if (this.buffer.byteLength >= len) { - return Promise.resolve() - } - return this.#fillTo(len) - } - - get byteLength(): number { - return this.buffer.byteLength - } - - // Adds more data to the buffer, returning true if more data was added. - async #fill(): Promise { - const result = await this.reader.read() - if (result.done) { - return false - } - const buffer = new Uint8Array(result.value) - - if (this.buffer.byteLength == 0) { - this.buffer = buffer - } else { - const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) - temp.set(this.buffer) - temp.set(buffer, this.buffer.byteLength) - this.buffer = temp - } - - return true - } - - // Add more data to the buffer until it's at least size bytes. - async #fillTo(size: number) { - while (this.buffer.byteLength < size) { - if (!(await this.#fill())) { - throw new Error("unexpected end of stream") - } - } - } - - // Consumes the first size bytes of the buffer. - #slice(size: number): Uint8Array { - const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) - this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) - - return result - } - - async read(size: number): Promise { - if (size == 0) return new Uint8Array() - - await this.#fillTo(size) - return this.#slice(size) - } - - async readAll(): Promise { - // eslint-disable-next-line no-empty - while (await this.#fill()) { } - return this.#slice(this.buffer.byteLength) - } - - async getUtf8String(maxLength?: number): Promise { - const length = await this.getNumberVarInt() - if (maxLength !== undefined && length > maxLength) { - throw new Error(`string length ${length} exceeds max length ${maxLength}`) - } - - const buffer = await this.read(length) - return new TextDecoder().decode(buffer) - } - - async getU8(): Promise { - await this.#fillTo(1) - return this.#slice(1)[0] - } - - async getU16(): Promise { - await this.#fillTo(2) - const slice = this.#slice(2) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return view.getInt16(0) - } - - // Returns a Number using 53-bits, the max Javascript can use for integer math - async getNumberVarInt(): Promise { - const v = await this.getVarInt() - if (v > MAX_U53) { - throw new Error("value larger than 53-bits; use v62 instead") - } - - return Number(v) - } - - async getVarInt(): Promise { - await this.#fillTo(1) - const size = (this.buffer[0] & 0xc0) >> 6 - - if (size == 0) { - const first = this.#slice(1)[0] - return BigInt(first) & 0x3fn - } else if (size == 1) { - await this.#fillTo(2) - const slice = this.#slice(2) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return BigInt(view.getInt16(0)) & 0x3fffn - } else if (size == 2) { - await this.#fillTo(4) - const slice = this.#slice(4) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return BigInt(view.getUint32(0)) & 0x3fffffffn - } else if (size == 3) { - await this.#fillTo(8) - const slice = this.#slice(8) - const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) - - return view.getBigUint64(0) & 0x3fffffffffffffffn - } else { - throw new Error("impossible") - } - } - - async getVarBytes(): Promise { - const length = await this.getNumberVarInt() - return this.read(length) - } - - - async done(): Promise { - if (this.buffer.byteLength > 0) return false - return !(await this.#fill()) - } - - async close() { - this.reader.releaseLock() - await this.readableStream.cancel() - } - - release(): [Uint8Array, ReadableStream] { - this.reader.releaseLock() - return [this.buffer, this.readableStream] - } - + protected buffer: Uint8Array + protected reader: ReadableStreamDefaultReader + protected readableStream: ReadableStream + + constructor(reader: ReadableStream, buffer?: Uint8Array) { + this.buffer = buffer ?? new Uint8Array() + this.reader = reader.getReader() + this.readableStream = reader + } + + waitForBytes(len: number): Promise { + if (this.buffer.byteLength >= len) { + return Promise.resolve() + } + return this.#fillTo(len) + } + + get byteLength(): number { + return this.buffer.byteLength + } + + // Adds more data to the buffer, returning true if more data was added. + async #fill(): Promise { + const result = await this.reader.read() + if (result.done) { + return false + } + const buffer = new Uint8Array(result.value) + + if (this.buffer.byteLength == 0) { + this.buffer = buffer + } else { + const temp = new Uint8Array(this.buffer.byteLength + buffer.byteLength) + temp.set(this.buffer) + temp.set(buffer, this.buffer.byteLength) + this.buffer = temp + } + + return true + } + + // Add more data to the buffer until it's at least size bytes. + async #fillTo(size: number) { + while (this.buffer.byteLength < size) { + if (!(await this.#fill())) { + throw new Error("unexpected end of stream") + } + } + } + + // Consumes the first size bytes of the buffer. + #slice(size: number): Uint8Array { + const result = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, size) + this.buffer = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset + size) + + return result + } + + async read(size: number): Promise { + if (size == 0) return new Uint8Array() + + await this.#fillTo(size) + return this.#slice(size) + } + + async readAll(): Promise { + // eslint-disable-next-line no-empty + while (await this.#fill()) {} + return this.#slice(this.buffer.byteLength) + } + + async getUtf8String(maxLength?: number): Promise { + const length = await this.getNumberVarInt() + if (maxLength !== undefined && length > maxLength) { + throw new Error(`string length ${length} exceeds max length ${maxLength}`) + } + + const buffer = await this.read(length) + return new TextDecoder().decode(buffer) + } + + async getU8(): Promise { + await this.#fillTo(1) + return this.#slice(1)[0] + } + + async getU16(): Promise { + await this.#fillTo(2) + const slice = this.#slice(2) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return view.getInt16(0) + } + + // Returns a Number using 53-bits, the max Javascript can use for integer math + async getNumberVarInt(): Promise { + const v = await this.getVarInt() + if (v > MAX_U53) { + throw new Error("value larger than 53-bits; use v62 instead") + } + + return Number(v) + } + + async getVarInt(): Promise { + await this.#fillTo(1) + const size = (this.buffer[0] & 0xc0) >> 6 + + if (size == 0) { + const first = this.#slice(1)[0] + return BigInt(first) & 0x3fn + } else if (size == 1) { + await this.#fillTo(2) + const slice = this.#slice(2) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return BigInt(view.getInt16(0)) & 0x3fffn + } else if (size == 2) { + await this.#fillTo(4) + const slice = this.#slice(4) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return BigInt(view.getUint32(0)) & 0x3fffffffn + } else if (size == 3) { + await this.#fillTo(8) + const slice = this.#slice(8) + const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength) + + return view.getBigUint64(0) & 0x3fffffffffffffffn + } else { + throw new Error("impossible") + } + } + + async getVarBytes(): Promise { + const length = await this.getNumberVarInt() + return this.read(length) + } + + async done(): Promise { + if (this.buffer.byteLength > 0) return false + return !(await this.#fill()) + } + + async close() { + this.reader.releaseLock() + await this.readableStream.cancel() + } + + release(): [Uint8Array, ReadableStream] { + this.reader.releaseLock() + return [this.buffer, this.readableStream] + } } export class WritableStreamBuffer implements Writer { - protected writer: WritableStreamDefaultWriter - protected writableStream: WritableStream - protected buffer: MutableBytesBuffer - constructor(writer: WritableStream) { - this.writer = writer.getWriter() - this.writableStream = writer - this.buffer = new MutableBytesBuffer(new Uint8Array()) - } - - async write(data: Uint8Array) { - return this.writer.write(data) - } - - async close() { - this.writer.releaseLock() - return this.writableStream.close() - } - - async flush() { - await this.write(this.buffer.Uint8Array) - this.clear() - } - - clear() { - this.buffer = new MutableBytesBuffer(new Uint8Array()) - } - - putU8(v: number) { - this.buffer.putU8(v) - } - - putU16(v: number) { - this.buffer.putU16(v) - } - - putVarInt(v: number | bigint) { - this.buffer.putVarInt(v) - } - - putUtf8String(v: string) { - this.buffer.putUtf8String(v) - } - - release(): [Uint8Array, WritableStream] { - this.writer.releaseLock() - return [this.buffer.Uint8Array, this.writableStream] - } + protected writer: WritableStreamDefaultWriter + protected writableStream: WritableStream + protected buffer: MutableBytesBuffer + constructor(writer: WritableStream) { + this.writer = writer.getWriter() + this.writableStream = writer + this.buffer = new MutableBytesBuffer(new Uint8Array()) + } + + async write(data: Uint8Array) { + return this.writer.write(data) + } + + async close() { + this.writer.releaseLock() + return this.writableStream.close() + } + + async flush() { + await this.write(this.buffer.Uint8Array) + this.clear() + } + + clear() { + this.buffer = new MutableBytesBuffer(new Uint8Array()) + } + + putU8(v: number) { + this.buffer.putU8(v) + } + + putU16(v: number) { + this.buffer.putU16(v) + } + + putVarInt(v: number | bigint) { + this.buffer.putVarInt(v) + } + + putUtf8String(v: string) { + this.buffer.putUtf8String(v) + } + + release(): [Uint8Array, WritableStream] { + this.writer.releaseLock() + return [this.buffer.Uint8Array, this.writableStream] + } } export class ReadableWritableStreamBuffer implements Reader, Writer { - private readStreamBuffer: ReadableStreamBuffer - private writeStreamBuffer: WritableStreamBuffer - - constructor(reader: ReadableStream, writer: WritableStream) { - this.readStreamBuffer = new ReadableStreamBuffer(reader) - this.writeStreamBuffer = new WritableStreamBuffer(writer) - } - - waitForBytes(len: number): Promise { - return this.readStreamBuffer.waitForBytes(len) - } - - get byteLength(): number { - return this.readStreamBuffer.byteLength - } - - async read(len: number): Promise { - return this.readStreamBuffer.read(len) - } - - - async getU8(): Promise { - return this.readStreamBuffer.getU8() - } - - async getU16(): Promise { - return this.readStreamBuffer.getU16() - } - - async getNumberVarInt(): Promise { - return this.readStreamBuffer.getNumberVarInt() - } - - async getVarInt(): Promise { - return this.readStreamBuffer.getVarInt() - } - - async getUtf8String(): Promise { - return this.readStreamBuffer.getUtf8String() - } - - async getVarBytes(): Promise { - return this.readStreamBuffer.getVarBytes() - } - - async done(): Promise { - return this.readStreamBuffer.done() - } - - putU16(v: number): void { - this.writeStreamBuffer.putU16(v) - } - putVarInt(v: number | bigint): void { - this.writeStreamBuffer.putVarInt(v) - } - putUtf8String(v: string): void { - this.writeStreamBuffer.putUtf8String(v) - } - putU8(v: number) { - this.writeStreamBuffer.putU8(v) - } - - async write(data: Uint8Array) { - return this.writeStreamBuffer.write(data) - } - - async close() { - this.readStreamBuffer.close() - this.writeStreamBuffer.close() - } - async flush(): Promise { - return this.writeStreamBuffer.flush() - } - - clear(): void { - this.writeStreamBuffer.clear() - } - - - release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] { - throw new Error("use release all instead of release") - } - - releaseAll(): [Uint8Array, Uint8Array, ReadableStream, WritableStream] { - const [readBuffer, readStream] = this.readStreamBuffer.release() - const [writeBuffer, writeStream] = this.writeStreamBuffer.release() - return [readBuffer, writeBuffer, readStream, writeStream] - } + private readStreamBuffer: ReadableStreamBuffer + private writeStreamBuffer: WritableStreamBuffer + + constructor(reader: ReadableStream, writer: WritableStream) { + this.readStreamBuffer = new ReadableStreamBuffer(reader) + this.writeStreamBuffer = new WritableStreamBuffer(writer) + } + + waitForBytes(len: number): Promise { + return this.readStreamBuffer.waitForBytes(len) + } + + get byteLength(): number { + return this.readStreamBuffer.byteLength + } + + async read(len: number): Promise { + return this.readStreamBuffer.read(len) + } + + async getU8(): Promise { + return this.readStreamBuffer.getU8() + } + + async getU16(): Promise { + return this.readStreamBuffer.getU16() + } + + async getNumberVarInt(): Promise { + return this.readStreamBuffer.getNumberVarInt() + } + + async getVarInt(): Promise { + return this.readStreamBuffer.getVarInt() + } + + async getUtf8String(): Promise { + return this.readStreamBuffer.getUtf8String() + } + + async getVarBytes(): Promise { + return this.readStreamBuffer.getVarBytes() + } + + async done(): Promise { + return this.readStreamBuffer.done() + } + + putU16(v: number): void { + this.writeStreamBuffer.putU16(v) + } + putVarInt(v: number | bigint): void { + this.writeStreamBuffer.putVarInt(v) + } + putUtf8String(v: string): void { + this.writeStreamBuffer.putUtf8String(v) + } + putU8(v: number) { + this.writeStreamBuffer.putU8(v) + } + + async write(data: Uint8Array) { + return this.writeStreamBuffer.write(data) + } + + async close() { + this.readStreamBuffer.close() + this.writeStreamBuffer.close() + } + async flush(): Promise { + return this.writeStreamBuffer.flush() + } + + clear(): void { + this.writeStreamBuffer.clear() + } + + release(): [Uint8Array, ReadableStream] | [Uint8Array, WritableStream] { + throw new Error("use release all instead of release") + } + + releaseAll(): [Uint8Array, Uint8Array, ReadableStream, WritableStream] { + const [readBuffer, readStream] = this.readStreamBuffer.release() + const [writeBuffer, writeStream] = this.writeStreamBuffer.release() + return [readBuffer, writeBuffer, readStream, writeStream] + } } function nextPow2(x: number): number { - // Handle edge cases - if (x <= 1) return 1; - - // Decrement to handle exact powers of 2 - x--; - - // Fill all bits below the highest set bit - x = x | (x >> 1); - x = x | (x >> 2); - x = x | (x >> 4); - x = x | (x >> 8); - x = x | (x >> 16); - - // Increment to get next power of 2 - return x + 1; -} \ No newline at end of file + // Handle edge cases + if (x <= 1) return 1 + + // Decrement to handle exact powers of 2 + x-- + + // Fill all bits below the highest set bit + x = x | (x >> 1) + x = x | (x >> 2) + x = x | (x >> 4) + x = x | (x >> 8) + x = x | (x >> 16) + + // Increment to get next power of 2 + return x + 1 +} diff --git a/lib/transport/client.ts b/lib/transport/client.ts index 0440cbb..115edd0 100644 --- a/lib/transport/client.ts +++ b/lib/transport/client.ts @@ -1,5 +1,5 @@ import * as Control from "./control" -import * as Stream from './stream' +import * as Stream from "./stream" import { Objects } from "./objects" import { Connection } from "./connection" import { ClientSetup, ControlMessageType, ServerSetup } from "./control" @@ -79,7 +79,8 @@ export class Client { async readServerSetup(buffer: ReadableWritableStreamBuffer): Promise { const type: ControlMessageType = await buffer.getNumberVarInt() - if (type !== ControlMessageType.ServerSetup) throw new Error(`server SETUP type must be ${ControlMessageType.ServerSetup}, got ${type}`) + if (type !== ControlMessageType.ServerSetup) + throw new Error(`server SETUP type must be ${ControlMessageType.ServerSetup}, got ${type}`) const advertisedLength = await buffer.getU16() const bufferLen = buffer.byteLength @@ -96,7 +97,8 @@ export class Client { async readClientSetup(buffer: ReadableWritableStreamBuffer): Promise { const type: ControlMessageType = await buffer.getNumberVarInt() - if (type !== ControlMessageType.ClientSetup) throw new Error(`client SETUP type must be ${ControlMessageType.ClientSetup}, got ${type}`) + if (type !== ControlMessageType.ClientSetup) + throw new Error(`client SETUP type must be ${ControlMessageType.ClientSetup}, got ${type}`) const advertisedLength = await buffer.getU16() const bufferLen = buffer.byteLength diff --git a/lib/transport/connection.ts b/lib/transport/connection.ts index 418f376..edd27f7 100644 --- a/lib/transport/connection.ts +++ b/lib/transport/connection.ts @@ -68,7 +68,7 @@ export class Connection { // Receive messages until the connection is closed. try { console.log("starting control loop") - for (; ;) { + for (;;) { const msg = await this.#controlStream.recv() await this.#recv(msg) } @@ -81,7 +81,7 @@ export class Connection { async #runObjects() { try { console.log("starting object loop") - for (; ;) { + for (;;) { const obj = await this.#objects.recv() console.log("object loop got obj", obj) if (!obj) break diff --git a/lib/transport/control/client_setup.ts b/lib/transport/control/client_setup.ts index a1ac3f8..4067079 100644 --- a/lib/transport/control/client_setup.ts +++ b/lib/transport/control/client_setup.ts @@ -25,7 +25,7 @@ export namespace ClientSetup { const numParams = reader.getNumberVarInt() const params = Parameters.deserialize_with_count(reader, numParams) return { - params + params, } } } diff --git a/lib/transport/control/fetch.ts b/lib/transport/control/fetch.ts index 088bd63..e149339 100644 --- a/lib/transport/control/fetch.ts +++ b/lib/transport/control/fetch.ts @@ -37,7 +37,6 @@ export interface StandaloneFetch { end_location: Location } - export namespace StandaloneFetch { export function serialize(v: StandaloneFetch): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) @@ -62,12 +61,11 @@ export namespace StandaloneFetch { namespace, name, start_location, - end_location + end_location, } } } - export interface JoiningFetch { id: bigint start: bigint @@ -91,12 +89,11 @@ export namespace JoiningFetch { const start = r.getVarInt() return { id, - start + start, } } } - export interface Fetch { id: bigint fetch_type: FetchType @@ -147,7 +144,7 @@ export namespace Fetch { fetch_type, standalone, joining, - params + params, } } } diff --git a/lib/transport/control/fetch_cancel.ts b/lib/transport/control/fetch_cancel.ts index a6f4073..cc62ad1 100644 --- a/lib/transport/control/fetch_cancel.ts +++ b/lib/transport/control/fetch_cancel.ts @@ -1,29 +1,26 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - export interface FetchCancel { - id: bigint + id: bigint } - export namespace FetchCancel { - export function serialize(v: FetchCancel): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.FetchCancel) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) + export function serialize(v: FetchCancel): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.FetchCancel) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } - - export function deserialize(reader: ImmutableBytesBuffer): FetchCancel { - const id = reader.getVarInt() - return { - id - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): FetchCancel { + const id = reader.getVarInt() + return { + id, + } + } +} diff --git a/lib/transport/control/fetch_ok.ts b/lib/transport/control/fetch_ok.ts index 984fad2..ea6f67a 100644 --- a/lib/transport/control/fetch_ok.ts +++ b/lib/transport/control/fetch_ok.ts @@ -51,7 +51,7 @@ export namespace FetchOk { end_of_track, end_location, track_extensions, - params + params, } } } diff --git a/lib/transport/control/go_away.ts b/lib/transport/control/go_away.ts index eb64f6b..5e87d0a 100644 --- a/lib/transport/control/go_away.ts +++ b/lib/transport/control/go_away.ts @@ -1,28 +1,26 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - export interface GoAway { - session_uri: string + session_uri: string } - export namespace GoAway { - export function serialize(v: GoAway): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.GoAway) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putUtf8String(v.session_uri) + export function serialize(v: GoAway): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.GoAway) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putUtf8String(v.session_uri) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): GoAway { - const session_uri = reader.getUtf8String() - return { - session_uri - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): GoAway { + const session_uri = reader.getUtf8String() + return { + session_uri, + } + } +} diff --git a/lib/transport/control/index.ts b/lib/transport/control/index.ts index e78b666..895c279 100644 --- a/lib/transport/control/index.ts +++ b/lib/transport/control/index.ts @@ -23,7 +23,6 @@ import { RequestError } from "./request_error" import { MaxRequestId } from "./max_request_id" import { RequestsBlocked } from "./requests_blocked" - enum Version { DRAFT_00 = 0xff000000, DRAFT_01 = 0xff000001, @@ -66,13 +65,35 @@ type MessageWithType = type Message = Subscriber | Publisher // Sent by subscriber -type Subscriber = Subscribe | SubscribeUpdate | SubscribeNamespace | - Unsubscribe | PublishOk | Fetch | FetchCancel | PublishNamespaceCancel | - TrackStatus | MaxRequestId | RequestsBlocked | RequestOk | RequestError +type Subscriber = + | Subscribe + | SubscribeUpdate + | SubscribeNamespace + | Unsubscribe + | PublishOk + | Fetch + | FetchCancel + | PublishNamespaceCancel + | TrackStatus + | MaxRequestId + | RequestsBlocked + | RequestOk + | RequestError // Sent by publisher -type Publisher = SubscribeOk | PublishDone | Publish | PublishNamespace | PublishNamespaceDone - | Namespace | NamespaceDone | FetchOk | MaxRequestId | RequestsBlocked | RequestOk | RequestError +type Publisher = + | SubscribeOk + | PublishDone + | Publish + | PublishNamespace + | PublishNamespaceDone + | Namespace + | NamespaceDone + | FetchOk + | MaxRequestId + | RequestsBlocked + | RequestOk + | RequestError function isSubscriber(m: ControlMessageType): boolean { return ( @@ -139,42 +160,66 @@ export enum ControlMessageType { ServerSetup = 0x21, // Legacy aliases for backward compat during transition - SubscribeUpdate = 0x2, // Same as RequestUpdate in draft-16 + SubscribeUpdate = 0x2, // Same as RequestUpdate in draft-16 } export namespace ControlMessageType { export function toString(t: ControlMessageType): string { switch (t) { - case ControlMessageType.ReservedSetupV00: return "ReservedSetupV00" - case ControlMessageType.GoAway: return "GoAway" - case ControlMessageType.MaxRequestId: return "MaxRequestId" - case ControlMessageType.RequestsBlocked: return "RequestsBlocked" - case ControlMessageType.RequestUpdate: return "RequestUpdate" - case ControlMessageType.Subscribe: return "Subscribe" - case ControlMessageType.SubscribeOk: return "SubscribeOk" - case ControlMessageType.RequestError: return "RequestError" - case ControlMessageType.Unsubscribe: return "Unsubscribe" - case ControlMessageType.PublishDone: return "PublishDone" - case ControlMessageType.PublishNamespaceCancel: return "PublishNamespaceCancel" - case ControlMessageType.TrackStatus: return "TrackStatus" - case ControlMessageType.NamespaceDone: return "NamespaceDone" - case ControlMessageType.Publish: return "Publish" - case ControlMessageType.PublishOk: return "PublishOk" - case ControlMessageType.PublishNamespace: return "PublishNamespace" - case ControlMessageType.RequestOk: return "RequestOk" - case ControlMessageType.Namespace: return "Namespace" - case ControlMessageType.PublishNamespaceDone: return "PublishNamespaceDone" - case ControlMessageType.SubscribeNamespace: return "SubscribeNamespace" - case ControlMessageType.Fetch: return "Fetch" - case ControlMessageType.FetchCancel: return "FetchCancel" - case ControlMessageType.FetchOk: return "FetchOk" - case ControlMessageType.ClientSetup: return "ClientSetup" - case ControlMessageType.ServerSetup: return "ServerSetup" + case ControlMessageType.ReservedSetupV00: + return "ReservedSetupV00" + case ControlMessageType.GoAway: + return "GoAway" + case ControlMessageType.MaxRequestId: + return "MaxRequestId" + case ControlMessageType.RequestsBlocked: + return "RequestsBlocked" + case ControlMessageType.RequestUpdate: + return "RequestUpdate" + case ControlMessageType.Subscribe: + return "Subscribe" + case ControlMessageType.SubscribeOk: + return "SubscribeOk" + case ControlMessageType.RequestError: + return "RequestError" + case ControlMessageType.Unsubscribe: + return "Unsubscribe" + case ControlMessageType.PublishDone: + return "PublishDone" + case ControlMessageType.PublishNamespaceCancel: + return "PublishNamespaceCancel" + case ControlMessageType.TrackStatus: + return "TrackStatus" + case ControlMessageType.NamespaceDone: + return "NamespaceDone" + case ControlMessageType.Publish: + return "Publish" + case ControlMessageType.PublishOk: + return "PublishOk" + case ControlMessageType.PublishNamespace: + return "PublishNamespace" + case ControlMessageType.RequestOk: + return "RequestOk" + case ControlMessageType.Namespace: + return "Namespace" + case ControlMessageType.PublishNamespaceDone: + return "PublishNamespaceDone" + case ControlMessageType.SubscribeNamespace: + return "SubscribeNamespace" + case ControlMessageType.Fetch: + return "Fetch" + case ControlMessageType.FetchCancel: + return "FetchCancel" + case ControlMessageType.FetchOk: + return "FetchOk" + case ControlMessageType.ClientSetup: + return "ClientSetup" + case ControlMessageType.ServerSetup: + return "ServerSetup" } } } - export { Subscribe, SubscribeOk, @@ -201,7 +246,6 @@ export { RequestsBlocked, RequestOk, RequestError, - Version, isSubscriber, isPublisher, diff --git a/lib/transport/control/max_request_id.ts b/lib/transport/control/max_request_id.ts index 2c58538..dee7c5c 100644 --- a/lib/transport/control/max_request_id.ts +++ b/lib/transport/control/max_request_id.ts @@ -1,29 +1,28 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - // Draft-16: MAX_REQUEST_ID (Section 9.5, type 0x15) // Sent to increase the number of requests the peer can send within a session. // The Maximum Request ID MUST only increase within a session. export interface MaxRequestId { - max_request_id: bigint + max_request_id: bigint } export namespace MaxRequestId { - export function serialize(v: MaxRequestId): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.MaxRequestId) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.max_request_id) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + export function serialize(v: MaxRequestId): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.MaxRequestId) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.max_request_id) + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): MaxRequestId { - const max_request_id = reader.getVarInt() - return { - max_request_id, - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): MaxRequestId { + const max_request_id = reader.getVarInt() + return { + max_request_id, + } + } +} diff --git a/lib/transport/control/namespace.ts b/lib/transport/control/namespace.ts index a381645..e739037 100644 --- a/lib/transport/control/namespace.ts +++ b/lib/transport/control/namespace.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple } from "../base_data" - export interface Namespace { namespace_suffix: string[] } diff --git a/lib/transport/control/namespace_done.ts b/lib/transport/control/namespace_done.ts index 66da359..5ba7386 100644 --- a/lib/transport/control/namespace_done.ts +++ b/lib/transport/control/namespace_done.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple } from "../base_data" - export interface NamespaceDone { namespace_suffix: string[] } diff --git a/lib/transport/control/publish.ts b/lib/transport/control/publish.ts index 383ece1..889d8d6 100644 --- a/lib/transport/control/publish.ts +++ b/lib/transport/control/publish.ts @@ -55,7 +55,7 @@ export namespace Publish { namespace, name, track_extensions, - params + params, } } } diff --git a/lib/transport/control/publish_done.ts b/lib/transport/control/publish_done.ts index ccb6aa8..c1878c6 100644 --- a/lib/transport/control/publish_done.ts +++ b/lib/transport/control/publish_done.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { ReasonPhrase } from "../base_data" - export interface PublishDone { id: bigint code: bigint @@ -34,7 +33,7 @@ export namespace PublishDone { id, code, stream_count, - reason + reason, } } } diff --git a/lib/transport/control/publish_namespace.ts b/lib/transport/control/publish_namespace.ts index ac13926..06a4f29 100644 --- a/lib/transport/control/publish_namespace.ts +++ b/lib/transport/control/publish_namespace.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters, Tuple, KeyValuePairs } from "../base_data" - export interface PublishNamespace { id: bigint namespace: Tuple @@ -36,7 +35,7 @@ export namespace PublishNamespace { return { id, namespace, - params + params, } } } diff --git a/lib/transport/control/publish_namespace_cancel.ts b/lib/transport/control/publish_namespace_cancel.ts index 7640cd5..eaafb01 100644 --- a/lib/transport/control/publish_namespace_cancel.ts +++ b/lib/transport/control/publish_namespace_cancel.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { ReasonPhrase } from "../base_data" - export interface PublishNamespaceCancel { id: bigint error_code: bigint @@ -29,7 +28,7 @@ export namespace PublishNamespaceCancel { return { id, error_code, - error_reason + error_reason, } } } diff --git a/lib/transport/control/publish_ok.ts b/lib/transport/control/publish_ok.ts index d94fbf0..8494f38 100644 --- a/lib/transport/control/publish_ok.ts +++ b/lib/transport/control/publish_ok.ts @@ -2,8 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters } from "../base_data" - - export interface PublishOk { id: bigint // Request ID params?: Parameters @@ -32,7 +30,7 @@ export namespace PublishOk { } return { id, - params + params, } } } diff --git a/lib/transport/control/request_error.ts b/lib/transport/control/request_error.ts index 75fb6f3..8574d42 100644 --- a/lib/transport/control/request_error.ts +++ b/lib/transport/control/request_error.ts @@ -5,9 +5,9 @@ import { ReasonPhrase } from "../base_data" // Draft-16: REQUEST_ERROR (Section 9.8) // Sent in response to any request (SUBSCRIBE, FETCH, PUBLISH, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS) export interface RequestError { - id: bigint // Request ID - code: bigint // Error Code (RequestErrorCode) - retry_interval: bigint // Minimum retry time in ms + 1; 0 = don't retry + id: bigint // Request ID + code: bigint // Error Code (RequestErrorCode) + retry_interval: bigint // Minimum retry time in ms + 1; 0 = don't retry reason: ReasonPhrase } diff --git a/lib/transport/control/request_ok.ts b/lib/transport/control/request_ok.ts index 0017d28..54eead0 100644 --- a/lib/transport/control/request_ok.ts +++ b/lib/transport/control/request_ok.ts @@ -5,32 +5,32 @@ import { Parameters } from "../base_data" // Draft-16: REQUEST_OK (Section 9.7) // Sent in response to REQUEST_UPDATE, TRACK_STATUS, SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE export interface RequestOk { - id: bigint // Request ID - parameters: Parameters + id: bigint // Request ID + parameters: Parameters } export namespace RequestOk { - export function serialize(v: RequestOk): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.RequestOk) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) - const paramsBytes = Parameters.serialize(v.parameters) - payloadBuf.putVarInt(v.parameters.size) // Number of Parameters - payloadBuf.putBytes(paramsBytes) + export function serialize(v: RequestOk): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.RequestOk) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) + const paramsBytes = Parameters.serialize(v.parameters) + payloadBuf.putVarInt(v.parameters.size) // Number of Parameters + payloadBuf.putBytes(paramsBytes) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): RequestOk { - const id = reader.getVarInt() - const numParams = reader.getNumberVarInt() - const parameters = Parameters.deserialize_with_count(reader, numParams) - return { - id, - parameters, - } - } + export function deserialize(reader: ImmutableBytesBuffer): RequestOk { + const id = reader.getVarInt() + const numParams = reader.getNumberVarInt() + const parameters = Parameters.deserialize_with_count(reader, numParams) + return { + id, + parameters, + } + } } diff --git a/lib/transport/control/requests_blocked.ts b/lib/transport/control/requests_blocked.ts index 10050b0..eda0548 100644 --- a/lib/transport/control/requests_blocked.ts +++ b/lib/transport/control/requests_blocked.ts @@ -1,7 +1,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - // Draft-16: REQUESTS_BLOCKED (Section 9.6, type 0x1A) // Sent when an endpoint would like to send a new request, but cannot // because the Request ID would exceed the Maximum Request ID value sent by the peer. diff --git a/lib/transport/control/server_setup.ts b/lib/transport/control/server_setup.ts index 550c954..90b5967 100644 --- a/lib/transport/control/server_setup.ts +++ b/lib/transport/control/server_setup.ts @@ -24,7 +24,7 @@ export namespace ServerSetup { const numParams = reader.getNumberVarInt() const params = Parameters.deserialize_with_count(reader, numParams) return { - params + params, } } } diff --git a/lib/transport/control/setup_parameters.ts b/lib/transport/control/setup_parameters.ts index 4bd8178..67d233a 100644 --- a/lib/transport/control/setup_parameters.ts +++ b/lib/transport/control/setup_parameters.ts @@ -1,8 +1,7 @@ export enum SetupParameters { - Authority = 0x05, - Path = 0x01, - MaxRequestId = 0x02, - MaxAuthTokenCacheSize = 0x04, - // TODO(itzmanish): implement more params from the draft + Authority = 0x05, + Path = 0x01, + MaxRequestId = 0x02, + MaxAuthTokenCacheSize = 0x04, + // TODO(itzmanish): implement more params from the draft } - diff --git a/lib/transport/control/subscribe.ts b/lib/transport/control/subscribe.ts index e04ae81..30c80f3 100644 --- a/lib/transport/control/subscribe.ts +++ b/lib/transport/control/subscribe.ts @@ -61,15 +61,13 @@ export namespace FilterType { } } - export interface Subscribe { id: bigint // Request ID - namespace: Tuple, + namespace: Tuple name: string params: Parameters } - export namespace Subscribe { export function serialize(v: Subscribe): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) diff --git a/lib/transport/control/subscribe_namespace.ts b/lib/transport/control/subscribe_namespace.ts index d930c51..ce02ff6 100644 --- a/lib/transport/control/subscribe_namespace.ts +++ b/lib/transport/control/subscribe_namespace.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple, Parameters, KeyValuePairs } from "../base_data" - export enum SubscribeOptions { PUBLISH = 0x00, NAMESPACE = 0x01, @@ -10,13 +9,12 @@ export enum SubscribeOptions { } export interface SubscribeNamespace { - id: bigint, + id: bigint namespace: string[] subscribe_options: SubscribeOptions params?: Parameters } - export namespace SubscribeNamespace { export function serialize(v: SubscribeNamespace): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) @@ -43,7 +41,7 @@ export namespace SubscribeNamespace { id, namespace, subscribe_options, - params + params, } } } diff --git a/lib/transport/control/subscribe_ok.ts b/lib/transport/control/subscribe_ok.ts index 50637bd..38cf2c5 100644 --- a/lib/transport/control/subscribe_ok.ts +++ b/lib/transport/control/subscribe_ok.ts @@ -3,7 +3,6 @@ import { GroupOrder } from "./subscribe" import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters, KeyValuePairs } from "../base_data" - export interface SubscribeOk { id: bigint // Request ID track_alias: bigint diff --git a/lib/transport/control/subscribe_update.ts b/lib/transport/control/subscribe_update.ts index 82ed7d3..4153038 100644 --- a/lib/transport/control/subscribe_update.ts +++ b/lib/transport/control/subscribe_update.ts @@ -3,12 +3,11 @@ import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Parameters, Location } from "../base_data" export interface SubscribeUpdate { - id: bigint // Request ID (new) - subscription_id: bigint // Existing Request ID + id: bigint // Request ID (new) + subscription_id: bigint // Existing Request ID params?: Parameters } - export namespace SubscribeUpdate { export function serialize(v: SubscribeUpdate): Uint8Array { const mainBuf = new MutableBytesBuffer(new Uint8Array()) diff --git a/lib/transport/control/track_status.ts b/lib/transport/control/track_status.ts index 4dab7e3..6e7aa94 100644 --- a/lib/transport/control/track_status.ts +++ b/lib/transport/control/track_status.ts @@ -2,7 +2,6 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" import { Tuple, Parameters } from "../base_data" - export interface TrackStatus { id: bigint namespace: Tuple diff --git a/lib/transport/control/unsubscribe.ts b/lib/transport/control/unsubscribe.ts index a092f82..581d9c0 100644 --- a/lib/transport/control/unsubscribe.ts +++ b/lib/transport/control/unsubscribe.ts @@ -1,27 +1,26 @@ import { ControlMessageType } from "." import { ImmutableBytesBuffer, MutableBytesBuffer } from "../buffer" - export interface Unsubscribe { - id: bigint + id: bigint } export namespace Unsubscribe { - export function serialize(v: Unsubscribe): Uint8Array { - const mainBuf = new MutableBytesBuffer(new Uint8Array()) - mainBuf.putVarInt(ControlMessageType.Unsubscribe) - const payloadBuf = new MutableBytesBuffer(new Uint8Array()) - payloadBuf.putVarInt(v.id) + export function serialize(v: Unsubscribe): Uint8Array { + const mainBuf = new MutableBytesBuffer(new Uint8Array()) + mainBuf.putVarInt(ControlMessageType.Unsubscribe) + const payloadBuf = new MutableBytesBuffer(new Uint8Array()) + payloadBuf.putVarInt(v.id) - mainBuf.putU16(payloadBuf.byteLength) - mainBuf.putBytes(payloadBuf.Uint8Array) - return mainBuf.Uint8Array - } + mainBuf.putU16(payloadBuf.byteLength) + mainBuf.putBytes(payloadBuf.Uint8Array) + return mainBuf.Uint8Array + } - export function deserialize(reader: ImmutableBytesBuffer): Unsubscribe { - const id = reader.getVarInt() - return { - id - } - } -} \ No newline at end of file + export function deserialize(reader: ImmutableBytesBuffer): Unsubscribe { + const id = reader.getVarInt() + return { + id, + } + } +} diff --git a/lib/transport/fetch.ts b/lib/transport/fetch.ts index ca9b492..6ad1504 100644 --- a/lib/transport/fetch.ts +++ b/lib/transport/fetch.ts @@ -1,25 +1,21 @@ - export interface FetchHeader { - type: FetchType - id: bigint + type: FetchType + id: bigint } - export enum FetchType { - Type0x5 = 0x5, + Type0x5 = 0x5, } export namespace FetchType { - export function try_from(value: number | bigint): FetchType { - const v = typeof value === "bigint" ? Number(value) : value + export function try_from(value: number | bigint): FetchType { + const v = typeof value === "bigint" ? Number(value) : value - switch (v) { - case FetchType.Type0x5: - return v as FetchType - default: - throw new Error(`invalid fetch type: ${v}`) - } - } + switch (v) { + case FetchType.Type0x5: + return v as FetchType + default: + throw new Error(`invalid fetch type: ${v}`) + } + } } - - diff --git a/lib/transport/objects.ts b/lib/transport/objects.ts index c8aa534..3fc65ed 100644 --- a/lib/transport/objects.ts +++ b/lib/transport/objects.ts @@ -1,7 +1,14 @@ import { SubgroupHeader, SubgroupObject, SubgroupReader, SubgroupType, SubgroupWriter } from "./subgroup" import { KeyValuePairs } from "./base_data" import { debug } from "./utils" -import { ImmutableBytesBuffer, MutableBytesBuffer, ReadableStreamBuffer, Reader, WritableStreamBuffer, Writer } from "./buffer" +import { + ImmutableBytesBuffer, + MutableBytesBuffer, + ReadableStreamBuffer, + Reader, + WritableStreamBuffer, + Writer, +} from "./buffer" export enum ObjectForwardingPreference { Datagram = "Datagram", @@ -57,7 +64,6 @@ export function isDatagram(obj: ObjectDatagram | SubgroupHeader): boolean { return (obj.type & 0x10) === 0 } - // Draft-16: Object Datagram types use bitmask structure 0b00X0XXXX // Valid ranges: 0x00..0x0F, 0x20..0x2F // Bit layout: @@ -118,7 +124,7 @@ export namespace ObjectDatagramType { throw new Error(`invalid object datagram type: ${v} (bit 4 set - this is a subgroup type)`) } // Must be in ranges 0x00..0x0F or 0x20..0x2F - if (v < 0x00 || (v > 0x0F && v < 0x20) || v > 0x2F) { + if (v < 0x00 || (v > 0x0f && v < 0x20) || v > 0x2f) { throw new Error(`invalid object datagram type: ${v} (out of range)`) } // STATUS + END_OF_GROUP is invalid @@ -155,7 +161,7 @@ export interface ObjectDatagram { track_alias: bigint group_id: number object_id?: number - publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set extension_headers?: KeyValuePairs status?: Status object_payload?: Uint8Array @@ -237,7 +243,7 @@ export class Objects { } async send(h: ObjectDatagram | SubgroupHeader): Promise { - const is_datagram = isDatagram(h); + const is_datagram = isDatagram(h) if (is_datagram) { // Datagram mode @@ -318,9 +324,7 @@ export class TrackWriter { // For compatibility with reader interface public header = { track_alias: 0n } - constructor( - public stream: Writer, - ) { } + constructor(public stream: Writer) {} async write(c: ObjectDatagram) { return this.stream.write(ObjectDatagram.serialize(c)) @@ -331,15 +335,11 @@ export class TrackWriter { } } - export class TrackReader { // Header with track_alias for routing public header: { track_alias: bigint } - constructor( - stream: Reader, - track_alias: bigint = 0n, - ) { + constructor(stream: Reader, track_alias: bigint = 0n) { this.stream = stream this.header = { track_alias } } @@ -392,4 +392,3 @@ export class TrackReader { await this.stream.close() } } - diff --git a/lib/transport/publisher.ts b/lib/transport/publisher.ts index 509d2cb..cec4d2d 100644 --- a/lib/transport/publisher.ts +++ b/lib/transport/publisher.ts @@ -55,20 +55,20 @@ export class Publisher { } async recv(msg: Control.MessageWithType) { - const { type, message } = msg; + const { type, message } = msg switch (type) { case Control.ControlMessageType.Subscribe: await this.recvSubscribe(message) - break; + break case Control.ControlMessageType.Unsubscribe: this.recvUnsubscribe(message) - break; + break case Control.ControlMessageType.RequestOk: this.recvRequestOk(message) - break; + break case Control.ControlMessageType.RequestError: this.recvRequestError(message) - break; + break default: throw new Error(`unknown control message`) // impossible } @@ -121,7 +121,7 @@ export class Publisher { code: 0n, retry_interval: 0n, reason: e.message, - } + }, }) throw e } @@ -151,7 +151,7 @@ export class PublishNamespaceSend { } async ok() { - for (; ;) { + for (;;) { const [state, next] = this.#state.value() if (state === "ack") return if (state instanceof Error) throw state @@ -162,7 +162,7 @@ export class PublishNamespaceSend { } async active() { - for (; ;) { + for (;;) { const [state, next] = this.#state.value() if (state instanceof Error) throw state if (!next) return @@ -199,7 +199,7 @@ export class SubscribeRecv { #objects: Objects #id: bigint #trackAlias: bigint // Publisher-specified in draft-14 - params: Parameters; + params: Parameters readonly namespace: string[] readonly track: string @@ -232,12 +232,20 @@ export class SubscribeRecv { id: this.#id, track_alias: this.#trackAlias, params: this.params, - } + }, }) } // Close the subscription with an error. - async close({ code = 0n, reason = "", unsubscribe = true }: { code?: bigint; reason?: string; unsubscribe?: boolean }) { + async close({ + code = 0n, + reason = "", + unsubscribe = true, + }: { + code?: bigint + reason?: string + unsubscribe?: boolean + }) { if (this.#state === "closed") return const acked = this.#state === "ack" this.#state = "closed" @@ -245,13 +253,13 @@ export class SubscribeRecv { if (!acked) { return this.#control.send({ type: Control.ControlMessageType.RequestError, - message: { id: this.#id, code, retry_interval: 0n, reason } + message: { id: this.#id, code, retry_interval: 0n, reason }, }) } if (unsubscribe) { return this.#control.send({ type: Control.ControlMessageType.Unsubscribe, - message: { id: this.#id } + message: { id: this.#id }, }) } } diff --git a/lib/transport/stream.ts b/lib/transport/stream.ts index 19b2fcc..39418d0 100644 --- a/lib/transport/stream.ts +++ b/lib/transport/stream.ts @@ -1,16 +1,27 @@ - import { ControlMessageType, - MessageWithType, Publish, - PublishDone, PublishNamespace, - PublishNamespaceDone, PublishNamespaceCancel, - PublishOk, Unsubscribe, - Fetch, FetchOk, FetchCancel, - Subscribe, SubscribeOk, - SubscribeUpdate, SubscribeNamespace, - Namespace, NamespaceDone, TrackStatus, - MaxRequestId, RequestsBlocked, - RequestOk, RequestError, + MessageWithType, + Publish, + PublishDone, + PublishNamespace, + PublishNamespaceDone, + PublishNamespaceCancel, + PublishOk, + Unsubscribe, + Fetch, + FetchOk, + FetchCancel, + Subscribe, + SubscribeOk, + SubscribeUpdate, + SubscribeNamespace, + Namespace, + NamespaceDone, + TrackStatus, + MaxRequestId, + RequestsBlocked, + RequestOk, + RequestError, } from "./control" import { debug } from "./utils" import { ImmutableBytesBuffer, ReadableWritableStreamBuffer, Reader, Writer } from "./buffer" @@ -227,7 +238,6 @@ export class Decoder { } return res - } } diff --git a/lib/transport/subgroup.ts b/lib/transport/subgroup.ts index ae81db7..8bf89ec 100644 --- a/lib/transport/subgroup.ts +++ b/lib/transport/subgroup.ts @@ -3,54 +3,54 @@ import { KeyValuePairs } from "./base_data" import { Status } from "./objects" export interface SubgroupHeader { - type: SubgroupType - track_alias: bigint - group_id: number - subgroup_id?: number - publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set + type: SubgroupType + track_alias: bigint + group_id: number + subgroup_id?: number + publisher_priority?: number // undefined when DEFAULT_PRIORITY bit is set } export namespace SubgroupHeader { - export function serialize(header: SubgroupHeader): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putBytes(SubgroupType.serialize(header.type)) - buf.putVarInt(header.track_alias) - buf.putVarInt(header.group_id) - if (SubgroupType.hasExplicitSubgroupId(header.type) && header.subgroup_id !== undefined) { - buf.putVarInt(header.subgroup_id) - } - if (!SubgroupType.hasDefaultPriority(header.type) && header.publisher_priority !== undefined) { - buf.putU8(header.publisher_priority) - } - return buf.Uint8Array - } + export function serialize(header: SubgroupHeader): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putBytes(SubgroupType.serialize(header.type)) + buf.putVarInt(header.track_alias) + buf.putVarInt(header.group_id) + if (SubgroupType.hasExplicitSubgroupId(header.type) && header.subgroup_id !== undefined) { + buf.putVarInt(header.subgroup_id) + } + if (!SubgroupType.hasDefaultPriority(header.type) && header.publisher_priority !== undefined) { + buf.putU8(header.publisher_priority) + } + return buf.Uint8Array + } } export interface SubgroupObject { - object_id: number - extension_headers?: KeyValuePairs - status?: Status // only if payload is null - object_payload?: Uint8Array + object_id: number + extension_headers?: KeyValuePairs + status?: Status // only if payload is null + object_payload?: Uint8Array } export namespace SubgroupObject { - export function serialize(obj: SubgroupObject): Uint8Array { - const buf = new MutableBytesBuffer(new Uint8Array()) - buf.putVarInt(obj.object_id) - - if (obj.extension_headers) { - const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) - buf.putVarInt(extHeadersBytes.length) - buf.putBytes(extHeadersBytes) - } - buf.putVarInt(obj.object_payload?.length ?? 0) - if (!obj.object_payload) { - buf.putVarInt(obj.status!) - } else { - buf.putBytes(obj.object_payload) - } - return buf.Uint8Array - } + export function serialize(obj: SubgroupObject): Uint8Array { + const buf = new MutableBytesBuffer(new Uint8Array()) + buf.putVarInt(obj.object_id) + + if (obj.extension_headers) { + const extHeadersBytes = KeyValuePairs.serialize(obj.extension_headers) + buf.putVarInt(extHeadersBytes.length) + buf.putBytes(extHeadersBytes) + } + buf.putVarInt(obj.object_payload?.length ?? 0) + if (!obj.object_payload) { + buf.putVarInt(obj.status!) + } else { + buf.putBytes(obj.object_payload) + } + return buf.Uint8Array + } } // Draft-16: Subgroup header types use bitmask structure 0b00X1XXXX @@ -62,158 +62,157 @@ export namespace SubgroupObject { // bit 4 (0x10) = always set (stream type marker) // bit 5 (0x20) = DEFAULT_PRIORITY (when set, priority field omitted) export enum SubgroupType { - // Base types (0x10..0x1D) - Type0x10 = 0x10, - Type0x11 = 0x11, - Type0x12 = 0x12, - Type0x13 = 0x13, - Type0x14 = 0x14, - Type0x15 = 0x15, - Type0x18 = 0x18, - Type0x19 = 0x19, - Type0x1A = 0x1A, - Type0x1B = 0x1B, - Type0x1C = 0x1C, - Type0x1D = 0x1D, - // DEFAULT_PRIORITY types (0x30..0x3D) - Type0x30 = 0x30, - Type0x31 = 0x31, - Type0x32 = 0x32, - Type0x33 = 0x33, - Type0x34 = 0x34, - Type0x35 = 0x35, - Type0x38 = 0x38, - Type0x39 = 0x39, - Type0x3A = 0x3A, - Type0x3B = 0x3B, - Type0x3C = 0x3C, - Type0x3D = 0x3D, + // Base types (0x10..0x1D) + Type0x10 = 0x10, + Type0x11 = 0x11, + Type0x12 = 0x12, + Type0x13 = 0x13, + Type0x14 = 0x14, + Type0x15 = 0x15, + Type0x18 = 0x18, + Type0x19 = 0x19, + Type0x1A = 0x1a, + Type0x1B = 0x1b, + Type0x1C = 0x1c, + Type0x1D = 0x1d, + // DEFAULT_PRIORITY types (0x30..0x3D) + Type0x30 = 0x30, + Type0x31 = 0x31, + Type0x32 = 0x32, + Type0x33 = 0x33, + Type0x34 = 0x34, + Type0x35 = 0x35, + Type0x38 = 0x38, + Type0x39 = 0x39, + Type0x3A = 0x3a, + Type0x3B = 0x3b, + Type0x3C = 0x3c, + Type0x3D = 0x3d, } export namespace SubgroupType { - // Bitmask constants - const EXTENSIONS_BIT = 0x01 - const SUBGROUP_ID_MASK = 0x06 - const END_OF_GROUP_BIT = 0x08 - const DEFAULT_PRIORITY_BIT = 0x20 - - export function serialize(type: SubgroupType): Uint8Array { - const w = new MutableBytesBuffer(new Uint8Array()) - w.putVarInt(type) - return w.Uint8Array - } - export function deserialize(reader: ImmutableBytesBuffer): SubgroupType { - return try_from(reader.getNumberVarInt()) - } - - // may throw if invalid value is provided - export function try_from(value: number | bigint): SubgroupType { - const v = typeof value === "bigint" ? Number(value) : value - - // Must match form 0b00X1XXXX (bit 4 set) - if ((v & 0x10) === 0) { - throw new Error(`invalid subgroup type: ${v} (bit 4 not set)`) - } - // Must be in ranges 0x10..0x1F or 0x30..0x3F - if (v < 0x10 || (v > 0x1F && v < 0x30) || v > 0x3F) { - throw new Error(`invalid subgroup type: ${v} (out of range)`) - } - // SUBGROUP_ID_MODE = 0b11 is reserved - if (((v & SUBGROUP_ID_MASK) >> 1) === 3) { - throw new Error(`invalid subgroup type: ${v} (reserved SUBGROUP_ID_MODE=0b11)`) - } - return v as SubgroupType - } - - export function isSubgroupIdPresent(type: SubgroupType) { - return ((type & SUBGROUP_ID_MASK) >> 1) === 2 - } - - export function hasExplicitSubgroupId(type: SubgroupType) { - return isSubgroupIdPresent(type) - } - - export function isSubgroupIdZero(type: SubgroupType) { - return ((type & SUBGROUP_ID_MASK) >> 1) === 0 - } - - export function isSubgroupFirstObjectId(type: SubgroupType) { - return ((type & SUBGROUP_ID_MASK) >> 1) === 1 - } - - export function isExtensionPresent(type: SubgroupType) { - return (type & EXTENSIONS_BIT) !== 0 - } - - export function contains_end_of_group(type: SubgroupType) { - return (type & END_OF_GROUP_BIT) !== 0 - } - - export function hasDefaultPriority(type: SubgroupType) { - return (type & DEFAULT_PRIORITY_BIT) !== 0 - } + // Bitmask constants + const EXTENSIONS_BIT = 0x01 + const SUBGROUP_ID_MASK = 0x06 + const END_OF_GROUP_BIT = 0x08 + const DEFAULT_PRIORITY_BIT = 0x20 + + export function serialize(type: SubgroupType): Uint8Array { + const w = new MutableBytesBuffer(new Uint8Array()) + w.putVarInt(type) + return w.Uint8Array + } + export function deserialize(reader: ImmutableBytesBuffer): SubgroupType { + return try_from(reader.getNumberVarInt()) + } + + // may throw if invalid value is provided + export function try_from(value: number | bigint): SubgroupType { + const v = typeof value === "bigint" ? Number(value) : value + + // Must match form 0b00X1XXXX (bit 4 set) + if ((v & 0x10) === 0) { + throw new Error(`invalid subgroup type: ${v} (bit 4 not set)`) + } + // Must be in ranges 0x10..0x1F or 0x30..0x3F + if (v < 0x10 || (v > 0x1f && v < 0x30) || v > 0x3f) { + throw new Error(`invalid subgroup type: ${v} (out of range)`) + } + // SUBGROUP_ID_MODE = 0b11 is reserved + if ((v & SUBGROUP_ID_MASK) >> 1 === 3) { + throw new Error(`invalid subgroup type: ${v} (reserved SUBGROUP_ID_MODE=0b11)`) + } + return v as SubgroupType + } + + export function isSubgroupIdPresent(type: SubgroupType) { + return (type & SUBGROUP_ID_MASK) >> 1 === 2 + } + + export function hasExplicitSubgroupId(type: SubgroupType) { + return isSubgroupIdPresent(type) + } + + export function isSubgroupIdZero(type: SubgroupType) { + return (type & SUBGROUP_ID_MASK) >> 1 === 0 + } + + export function isSubgroupFirstObjectId(type: SubgroupType) { + return (type & SUBGROUP_ID_MASK) >> 1 === 1 + } + + export function isExtensionPresent(type: SubgroupType) { + return (type & EXTENSIONS_BIT) !== 0 + } + + export function contains_end_of_group(type: SubgroupType) { + return (type & END_OF_GROUP_BIT) !== 0 + } + + export function hasDefaultPriority(type: SubgroupType) { + return (type & DEFAULT_PRIORITY_BIT) !== 0 + } } - export class SubgroupWriter { - constructor( - public header: SubgroupHeader, - public stream: Writer, - ) { } - - async write(c: SubgroupObject) { - return this.stream.write(SubgroupObject.serialize(c)) - } - - async close() { - return this.stream.close() - } + constructor( + public header: SubgroupHeader, + public stream: Writer, + ) {} + + async write(c: SubgroupObject) { + return this.stream.write(SubgroupObject.serialize(c)) + } + + async close() { + return this.stream.close() + } } export class SubgroupReader { - constructor( - public header: SubgroupHeader, - public stream: Reader, - ) { } - - async read(): Promise { - if (await this.stream.done()) { - return - } - - const object_id = await this.stream.getNumberVarInt() - - let extHeaders: KeyValuePairs | undefined - if (SubgroupType.isExtensionPresent(this.header.type)) { - const extHeadersBytesLength = await this.stream.getNumberVarInt() - const extHeadersData = await this.stream.read(extHeadersBytesLength) - extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) - } - - console.log("subgroup header", object_id, extHeaders, this.stream) - - let obj_payload_len = await this.stream.getNumberVarInt() - - let object_payload: Uint8Array | undefined - let status: Status | undefined - - console.log("subgroup read", object_id, obj_payload_len) - - if (obj_payload_len == 0) { - status = Status.try_from(await this.stream.getNumberVarInt()) - } else { - object_payload = await this.stream.read(obj_payload_len) - } - - console.log("read success??", object_id, status, extHeaders, object_payload) - return { - object_id, - status, - extension_headers: extHeaders, - object_payload, - } - } - - async close() { - await this.stream.close() - } + constructor( + public header: SubgroupHeader, + public stream: Reader, + ) {} + + async read(): Promise { + if (await this.stream.done()) { + return + } + + const object_id = await this.stream.getNumberVarInt() + + let extHeaders: KeyValuePairs | undefined + if (SubgroupType.isExtensionPresent(this.header.type)) { + const extHeadersBytesLength = await this.stream.getNumberVarInt() + const extHeadersData = await this.stream.read(extHeadersBytesLength) + extHeaders = KeyValuePairs.deserialize(new ImmutableBytesBuffer(extHeadersData)) + } + + console.log("subgroup header", object_id, extHeaders, this.stream) + + let obj_payload_len = await this.stream.getNumberVarInt() + + let object_payload: Uint8Array | undefined + let status: Status | undefined + + console.log("subgroup read", object_id, obj_payload_len) + + if (obj_payload_len == 0) { + status = Status.try_from(await this.stream.getNumberVarInt()) + } else { + object_payload = await this.stream.read(obj_payload_len) + } + + console.log("read success??", object_id, status, extHeaders, object_payload) + return { + object_id, + status, + extension_headers: extHeaders, + object_payload, + } + } + + async close() { + await this.stream.close() + } } diff --git a/lib/transport/subscriber.ts b/lib/transport/subscriber.ts index 055d196..e2f9a6f 100644 --- a/lib/transport/subscriber.ts +++ b/lib/transport/subscriber.ts @@ -40,7 +40,7 @@ export class Subscriber { } async recv(msg: Control.MessageWithType) { - const { type, message } = msg; + const { type, message } = msg switch (type) { case Control.ControlMessageType.PublishNamespace: await this.recvPublishNamespace(message) @@ -69,7 +69,7 @@ export class Subscriber { await this.#control.send({ type: Control.ControlMessageType.RequestOk, - message: { id: msg.id, parameters: new Map() } + message: { id: msg.id, parameters: new Map() }, }) const publishNamespace = new PublishNamespaceRecv(this.#control, msg.namespace, msg.id) @@ -91,16 +91,20 @@ export class Subscriber { id, namespace, subscribe_options: Control.SubscribeOptions.BOTH, - } + }, } await this.#control.send(msg) } - async subscribe(namespace: string[], track: string, opts?: { - forward?: number, - subscriber_priority?: number, - group_order?: Control.GroupOrder, - }) { + async subscribe( + namespace: string[], + track: string, + opts?: { + forward?: number + subscriber_priority?: number + group_order?: Control.GroupOrder + }, + ) { const id = this.#control.nextRequestId() const subscribe = new SubscribeSend(this.#control, id, namespace, track) @@ -126,7 +130,7 @@ export class Subscriber { namespace, name: track, params, - } + }, } await this.#control.send(subscription_req) @@ -240,7 +244,7 @@ export class PublishNamespaceRecv { // Send the control message. return this.#control.send({ type: Control.ControlMessageType.RequestOk, - message: { id: this.#id, parameters: new Map() } + message: { id: this.#id, parameters: new Map() }, }) } @@ -250,7 +254,7 @@ export class PublishNamespaceRecv { return this.#control.send({ type: Control.ControlMessageType.RequestError, - message: { id: this.#id, code, retry_interval: 0n, reason } + message: { id: this.#id, code, retry_interval: 0n, reason }, }) } } diff --git a/lib/transport/tsconfig.json b/lib/transport/tsconfig.json index 38c67ea..88da906 100644 --- a/lib/transport/tsconfig.json +++ b/lib/transport/tsconfig.json @@ -1,11 +1,9 @@ { "extends": "../tsconfig.json", - "include": [ - "." - ], + "include": ["."], "references": [ { "path": "../common" } ] -} \ No newline at end of file +} diff --git a/lib/transport/utils.ts b/lib/transport/utils.ts index f658e06..f46577e 100644 --- a/lib/transport/utils.ts +++ b/lib/transport/utils.ts @@ -1,7 +1,7 @@ export function debug(...msg: any[]) { - console.log("itzmanish:", ...msg) + console.log("itzmanish:", ...msg) } export async function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} \ No newline at end of file + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 5299650..08dc503 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -4,11 +4,7 @@ "moduleResolution": "node", "module": "es2022", "target": "es2022", - "lib": [ - "es2022", - "dom", - "dom.iterable" - ], + "lib": ["es2022", "dom", "dom.iterable"], "outDir": "./dist", "strict": true, "allowJs": false, @@ -18,7 +14,5 @@ "isolatedModules": false, "noEmit": false }, - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +} diff --git a/lib/video-moq/index.ts b/lib/video-moq/index.ts index e16df1d..8a90c1a 100644 --- a/lib/video-moq/index.ts +++ b/lib/video-moq/index.ts @@ -378,20 +378,20 @@ export class VideoMoq extends HTMLElement { public play(): Promise { return this.player ? this.player.play().then(() => { - if (!this.#playButton) return - this.#playButton.innerHTML = PAUSE_SVG - this.#playButton.ariaLabel = "Pause" - }) + if (!this.#playButton) return + this.#playButton.innerHTML = PAUSE_SVG + this.#playButton.ariaLabel = "Pause" + }) : Promise.resolve() } public pause(): Promise { return this.player ? this.player.pause().then(() => { - if (!this.#playButton) return - this.#playButton.innerHTML = PLAY_SVG - this.#playButton.ariaLabel = "Play" - }) + if (!this.#playButton) return + this.#playButton.innerHTML = PLAY_SVG + this.#playButton.ariaLabel = "Play" + }) : Promise.resolve() } @@ -420,23 +420,23 @@ export class VideoMoq extends HTMLElement { public unmute(): Promise { return this.player ? this.player.mute(false).then(() => { - if (!this.#volumeButton) return - this.#volumeButton.ariaLabel = "Mute" - this.#volumeButton.innerText = "🔊" - this.#volumeRange!.value = this.previousVolume.toString() - }) + if (!this.#volumeButton) return + this.#volumeButton.ariaLabel = "Mute" + this.#volumeButton.innerText = "🔊" + this.#volumeRange!.value = this.previousVolume.toString() + }) : Promise.resolve() } public mute(): Promise { return this.player ? this.player.mute(true).then(() => { - if (!this.#volumeButton) return - this.#volumeButton.ariaLabel = "Unmute" - this.#volumeButton.innerText = "🔇" - this.previousVolume = parseFloat(this.#volumeRange!.value) - this.#volumeRange!.value = "0" - }) + if (!this.#volumeButton) return + this.#volumeButton.ariaLabel = "Unmute" + this.#volumeButton.innerText = "🔇" + this.previousVolume = parseFloat(this.#volumeRange!.value) + this.#volumeRange!.value = "0" + }) : Promise.resolve() }