npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cc-emu/yaka-nest

v3.7.0

Published

NestJS microservice transport over Yaka (RakNet)

Readme

@cc-emu/yaka-nest

NestJS microservice transport over Yaka.

Installation

npm install @cc-emu/yaka-nest

Development

npm install
npm run build
npm test

Release

Trigger the release workflow manually and choose patch/minor/major.

Usage

Packet classes extend YakaSerializable and are decorated with @YakaPacket(id). A packet without an id is skipped (with a warning), which is handy for feature-gated packets. An optional { version } binds the class to a protocol version (see Protocol version).

@YakaPacket self-registers the class into a process-global registry at import time, so a packet ends up in the codec only when its module is actually loaded (i.e. when it is used). The server and the client both build their native codec from this registry — there is no central protocol object to maintain.

// packets.ts
import { YakaPacket, YakaReply, YakaSerializable, type OutStream, type InStream } from '@cc-emu/yaka-nest';

@YakaPacket(1002)
export class PongPacket extends YakaSerializable {
  reply = '';

  write(out: OutStream): void {
    out.writeString(this.reply);
  }

  read(input: InStream): void {
    this.reply = input.readString() ?? '';
  }
}

@YakaPacket(1003)
export class PingErrorPacket extends YakaSerializable {
  code = 0;

  write(out: OutStream): void {
    out.writeInt32(this.code);
  }

  read(input: InStream): void {
    this.code = input.readInt32();
  }
}

// Reply classes (above) must be declared before @YakaReply references them.
@YakaPacket(1001)
@YakaReply(PongPacket, PingErrorPacket)
export class PingPacket extends YakaSerializable {
  message = '';

  write(out: OutStream): void {
    out.writeString(this.message);
  }

  read(input: InStream): void {
    this.message = input.readString() ?? '';
  }
}

@YakaPacket(2001)
export class PlayerMoveEvent extends YakaSerializable {
  x = 0;
  y = 0;

  write(out: OutStream): void {
    out.writeFloat(this.x);
    out.writeFloat(this.y);
  }

  read(input: InStream): void {
    this.x = input.readFloat();
    this.y = input.readFloat();
  }
}

Reply contract

@YakaReply(...) declares the reply contract on a query class. It is read by the client to know which reply ids to await for request() / send(). The server ignores it — on the server, whether a reply is sent is decided purely by the handler return value (return a YakaSerializable and it is sent; return void and nothing is sent). Declare the reply classes before the query class, since the decorator evaluates at class-definition time.

  • One class maps to exactly one (id, version). The same numeric id may be shared by several classes across different versions (the overlay pattern).
  • Two different classes claiming the same (id, version) log a warning and the later one wins.
  • Naming convention: *Command, *Query, *Reply, *Event — the role and direction are visible from the class name.

Send options

Per-class send settings (priority / reliability / orderingChannel) are declared via the sendOptions option of YakaServerTransport and YakaClientProxy, as an array of [PacketClass, SendOptions] tuples. Classes without an entry send with the native defaults (MEDIUM_PRIORITY, RELIABLE_ORDERED, channel 0).

new YakaServerTransport({
  port: 19132,
  sendOptions: [[PongPacket, { priority: 0, reliability: 0 }]],
});

Protocol version

All packets live in a single codec keyed by (id, version): a class without { version } forms the default layer, a versioned class sits alongside it — a versioned packet and a default packet may share the same numeric id. At runtime the entry is resolved per connection: exact (id, connectionVersion) match wins, otherwise the default entry is used, otherwise deserialization fails. Registration is always version-agnostic — neither the server nor the client build their codec under a pinned version; version only participates in runtime resolution.

protocolVersion in YakaServerTransport options overrides the version used to resolve handlers at runtime for every connection (options.protocolVersion ?? client.protocolVersion): dispatch tries the exact { id, version } match first and falls back to the versionless @YakaQuery(Packet) handler, otherwise it logs a No handler for pattern warning. It does not touch packet registration or the underlying codec:

new YakaServerTransport({
  port: 19132,
  protocolVersion: '0.28.01.98479', // handler dispatch override for every client
});

Without the override each connection's version starts unset (default entries only) and is assigned from a handler once the client is identified:

@YakaQuery(AuthPacket)
handleAuth(data: AuthPacket, ctx: YakaContext): void {
  ctx.getConnection().setProtocolVersion(data.clientVersion);
}

Note: the first packet(s) a client sends must resolve against the default layer — a versioned packet arriving before the version is set falls back to the default entry with the same id, or fails to deserialize if there is none.

The client knows its version upfront and declares it via protocolVersion in YakaClientProxy options — it is forwarded to the YakaConnection automatically.

This is how a packet contract evolves — keep the common packets default, and declare only the changed shape as a versioned class:

// common packets -> default layer, work for every version
@YakaPacket(0x88a3)
@YakaReply(AuthResultPacket)
class AuthPacket extends YakaSerializable { /* legacy shape */ }

// changed shape in 0.28 -> versioned entry (shares id 0x88a3 with the legacy one)
@YakaPacket(0x88a3, { version: '0.28.01.98479' })
@YakaReply(AuthResultPacketV28)
class AuthPacketV28 extends YakaSerializable { /* 0.28 shape */ }

To override handler behavior for a specific revision within one process, decorate the versioned class — the route pattern comes from the class's @YakaPacket registration, so there is nothing to pass at the query level. Dispatch tries the exact { id, version } match first and falls back to the versionless @YakaQuery(Packet) default handler:

@YakaQuery(AuthPacket)
handleAuthDefault(data: AuthPacket, ctx: YakaContext): AuthResultPacket { /* shared path */ }

@YakaQuery(AuthPacketV28)
handleAuthV28(data: AuthPacketV28, ctx: YakaContext): AuthResultPacketV28 { /* v2-only path */ }

Server

// main.ts
import { NestFactory } from '@nestjs/core';
import { YakaServerTransport } from '@cc-emu/yaka-nest';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice(AppModule, {
    strategy: new YakaServerTransport({
      port: 19132,
      logLevel: 'debug',
    }),
  });
  await app.listen();
}
bootstrap();

A handler is invoked for every incoming packet. If it returns a YakaSerializable (or an array of them), each element is sent back to the client as a separate packet; if it returns void / null, nothing is sent (command semantics). There is no reply-contract validation on the server — return whatever packet you like:

// app.controller.ts
import { Controller } from '@nestjs/common';
import { YakaClient, YakaContext, YakaQuery } from '@cc-emu/yaka-nest';
import type { YakaConnection } from '@cc-emu/yaka';
import { PingPacket, PongPacket, PingErrorPacket, PlayerMoveEvent } from './packets';

@Controller()
export class AppController {
  // Returns a Serializable -> sent to the client.
  // @YakaClient() injects the connection that sent the packet.
  @YakaQuery(PingPacket)
  handlePing(data: PingPacket, @YakaClient() client: YakaConnection): PongPacket | PingErrorPacket {
    const response = new PongPacket();
    response.reply = `pong: ${data.message}`;
    return response;
  }

  // Returns void -> fire-and-forget command. The full context is available too.
  @YakaQuery(PlayerMoveEvent)
  handleMove(data: PlayerMoveEvent, context: YakaContext): void {
    const connection = context.getConnection();
  }

  // An array -> each element sent as a separate packet.
  @YakaQuery(PingPacket)
  handlePingBurst(): PongPacket[] {
    return [new PongPacket(), new PongPacket()];
  }
}

@YakaClient() is a parameter decorator that resolves to the YakaConnection of the client that sent the inbound packet (a shortcut for context.getConnection()). The full YakaContext is also available as the second handler argument.

Shared handlers

Several packet classes can share one handler — list them all on a single @YakaQuery. Disambiguate the incoming packet in the body via instanceof, data.constructor, or ctx.getPattern():

@YakaQuery(PingPacket, PlayerMoveEvent)
handleShared(data: PingPacket | PlayerMoveEvent, ctx: YakaContext): void {
  if (data instanceof PingPacket) {
    // ping path
  }
  // or branch on ctx.getPattern() — the matched packet id
}

Unregistered classes are warned about and dropped; if every listed class is unregistered the whole decorator is skipped.

Logging

Set logLevel: 'debug' to have the underlying YakaServer emit per-packet debug events (direction, packet id, length, totals, raw bytes). By default those events are bridged into the transport's NestJS Logger, formatted as a hex dump:

new YakaServerTransport({
  port: 19132,
  logLevel: 'debug',
});

Provide your own logger (any YakaLogger) to route debug/warn/error elsewhere:

import { createYakaLogger, YakaServerTransport } from '@cc-emu/yaka-nest';

new YakaServerTransport({
  port: 19132,
  logLevel: 'debug',
  logger: createYakaLogger(app.get(Logger)), // bridge any NestJS LoggerService
});

logLevel and logger are native YakaServer options and are forwarded as-is. The client (YakaClientProxy) logs through its own NestJS Logger.

Server-side errors from YakaServer are routed by type: YakaDeserializeError (malformed incoming packet) is logged as a warning, YakaSerializeError / YakaConnectionError and other errors are logged as errors. Error classes are re-exported from this package for instanceof checks.

Client

// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule } from '@nestjs/microservices';
import { YakaClientProxy } from '@cc-emu/yaka-nest';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'YAKA_SERVICE',
        customClass: YakaClientProxy,
        options: {
          host: 'localhost',
          port: 19132,
          protocolVersion: '0.28.01.98479',
          sendOptions: [[PingPacket, { priority: 0, reliability: 0 }]],
        },
      },
    ]),
  ],
})
export class AppModule {}
// game.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { YakaClientProxy } from '@cc-emu/yaka-nest';
import { PingPacket, PongPacket } from './packets';

@Injectable()
export class GameService {
  constructor(@Inject('YAKA_SERVICE') private readonly proxy: YakaClientProxy) {}

  async ping(message: string): Promise<PongPacket> {
    // request() reads @YakaReply from PingPacket and awaits the first match.
    return this.proxy.request<PongPacket>(new PingPacket(message)).toPromise();
  }

  movePlayer(data: { x: number; y: number }) {
    this.proxy.emit(String(2001), data);
  }
}

request() resolves with the first incoming packet whose id is among the query's declared @YakaReply. If the packet declares no @YakaReply, request() throws and send() is treated as fire-and-forget.

Yaka-native helpers

You can also use the fluent builder for fire-and-forget messages:

import { YakaClientProxy } from '@cc-emu/yaka-nest';

export class GameService {
  constructor(@Inject('YAKA_SERVICE') private readonly client: YakaClientProxy) {}

  sendPing(message: string) {
    this.client.to(PingPacket).emit({ message });
  }
}