# API Reference

Source: https://imqueue.org/api/

Application programming interface documentation for the @imqueue packages.
Packaging follows nesting, so importing @imqueue/rpc re-exports everything from
@imqueue/core.

Every page below, and every per-symbol page under it, is also published as plain
markdown at `<page-url>index.md` and at `<page>.md`, byte-identical — same content,
no HTML. To look a symbol up by
name, fetch https://imqueue.org/api/search-index.json: every exported symbol of the
current majors as `{name, kind, package, url, summary}`, with `deprecated: true`
on obsolete members.

## Generated reference — framework spine

- [@imqueue/rpc 3.5.3](https://imqueue.org/api/rpc/latest/) —
  services, clients and decorators (`@expose`, `@remote`, `@lock`, `@cache`) and
  the RPC runtime. [markdown](https://imqueue.org/api/rpc/latest/index.md)
- [@imqueue/core 3.3.2](https://imqueue.org/api/core/latest/) —
  the JSON messaging-queue engine, the IMQ factory and the pluggable adapter
  interface. [markdown](https://imqueue.org/api/core/latest/index.md)

These `/latest/` URLs always serve the current major, so a link keeps working
across releases. Archived majors are HTML-only and are not mirrored:
@imqueue/rpc 2.1.0 at https://imqueue.org/api/rpc/2.1.0/, @imqueue/rpc 1.17.1 at https://imqueue.org/api/rpc/1.17.1/, @imqueue/core 2.0.26 at https://imqueue.org/api/core/2.0.26/, @imqueue/core 1.15.0 at https://imqueue.org/api/core/1.15.0/.

## Generated reference — Data & events
- [@imqueue/pg-pubsub 3.0.5](https://imqueue.org/api/pg-pubsub/latest/) — Reliable PostgreSQL LISTEN/NOTIFY with inter-process lock support. Tagged `events`. [markdown](https://imqueue.org/api/pg-pubsub/latest/index.md)
- [@imqueue/pg-cache 5.0.6](https://imqueue.org/api/pg-cache/latest/) — PostgreSQL-managed cache on Redis for @imqueue service methods. Tagged `caching`. [markdown](https://imqueue.org/api/pg-cache/latest/index.md)
- [@imqueue/tag-cache 3.0.3](https://imqueue.org/api/tag-cache/latest/) — Tagged cache implementation over Redis. Tagged `caching`. [markdown](https://imqueue.org/api/tag-cache/latest/index.md)
- [@imqueue/pg-sequelize 4.2.0](https://imqueue.org/api/pg-sequelize/latest/) — Turns a query described as data — filters, paging, ordering and the requested fields — into one efficient Sequelize statement, with database views as models. Built on Sequelize v6 — mature, proven in production, and actively developed against here. If you would rather build on Prisma, @imqueue/pg-prisma covers the same ground for that stack. Tagged `ORM` (pick at most one package with this tag). [markdown](https://imqueue.org/api/pg-sequelize/latest/index.md)
- [@imqueue/pg-prisma 1.0.1](https://imqueue.org/api/pg-prisma/latest/) — Prisma query extensions — soft-delete, access scoping, authorship and audit trails — plus row archiving, change-notify triggers and down-migrations. Tagged `ORM` (pick at most one package with this tag). [markdown](https://imqueue.org/api/pg-prisma/latest/index.md)

## Generated reference — Observability
- [@imqueue/async-logger 3.2.0](https://imqueue.org/api/async-logger/latest/) — Non-blocking logger over winston, with file and HTTP transports configured from the environment. Tagged `logging`. [markdown](https://imqueue.org/api/async-logger/latest/index.md)
- [@imqueue/opentelemetry 4.0.0](https://imqueue.org/api/opentelemetry/latest/) — OpenTelemetry instrumentation for @imqueue/rpc — every RPC traced, with no changes to service or client code. Tagged `tracing & APM` (pick at most one package with this tag). [markdown](https://imqueue.org/api/opentelemetry/latest/index.md)
- [@imqueue/datadog 3.2.0](https://imqueue.org/api/datadog/latest/) — Datadog APM tracing for @imqueue/rpc — a drop-in replacement for Datadog's own dd-trace package that traces every RPC. Tagged `tracing & APM` (pick at most one package with this tag). [markdown](https://imqueue.org/api/datadog/latest/index.md)

## Generated reference — API composition
- [@imqueue/graphql-dependency 3.1.0](https://imqueue.org/api/graphql-dependency/latest/) — Declarative cross-service dependency loading for GraphQL — nested data in bulk instead of one call per resolved object. Tagged `GraphQL`. [markdown](https://imqueue.org/api/graphql-dependency/latest/index.md)
- [@imqueue/type-graphql-dependency 3.0.3](https://imqueue.org/api/type-graphql-dependency/latest/) — The same dependency loading for type-graphql — declared on your decorated classes rather than on raw GraphQL types. Tagged `GraphQL`. [markdown](https://imqueue.org/api/type-graphql-dependency/latest/index.md)

## Generated reference — Hardening & validation
- [@imqueue/net 3.0.2](https://imqueue.org/api/net/latest/) — CIDR membership testing for IPv4 and IPv6 — sorted binary ranges searched in O(log n) rather than one comparison per network. Tagged `network validation`. [markdown](https://imqueue.org/api/net/latest/index.md)
- [@imqueue/http-protect 3.0.1](https://imqueue.org/api/http-protect/latest/) — Per-IP rate limiting and banning for express-like servers, backed by Redis so every process sees one view of a client. Tagged `DDoS protection`. [markdown](https://imqueue.org/api/http-protect/latest/index.md)
- [@imqueue/validation 1.1.0](https://imqueue.org/api/validation/latest/) — Zod-backed field- and method-level validation via native (TC39) decorators. Tagged `input validation`. [markdown](https://imqueue.org/api/validation/latest/index.md)

## Generated reference — Background work
- [@imqueue/job 3.0.3](https://imqueue.org/api/job/latest/) — Safe-by-default Redis job queue — delayed and scheduled jobs, at-least-once delivery, and retries driven by the handler. Tagged `jobs & scheduling`. [markdown](https://imqueue.org/api/job/latest/index.md)## Introduction

`core` and `rpc` are the framework spine — the runtime API documented below, and
the two packages every @imqueue service is built on. Capability packages such as
`pg-pubsub` or `async-logger` layer on top of them, each publishing its own
generated reference under `/api/`. `cli` is different again: a rapid-development
command-line tool installed globally, documented by a handwritten manual rather
than a generated reference.

`rpc` re-exports the entire `@imqueue/core` surface, so a single import is enough
whether you use only the core features or the RPC features as well. For example,
these two imports are equivalent:

~~~typescript
import { profile, IMQMode } from '@imqueue/core';
import { profile, IMQMode } from '@imqueue/rpc';
~~~

Both work because `profile` and `IMQMode` are defined in `core`, which is a
dependency of `rpc`.

There is exactly one exception: `export *` never forwards a default export, so
core's default-exported [IMQ](https://imqueue.org/api/core/latest/core.imq/) factory is **not**
available from `rpc` — `import IMQ from '@imqueue/rpc'` yields `undefined`.
Import it from `@imqueue/core` directly.

This pairing is specific to `core` and `rpc`. The capability packages do not
re-export their @imqueue dependencies: `@imqueue/pg-cache` and the rest export
only their own surface, so a service using one still imports `@imqueue/rpc` for
the framework types.

> **Using v3?** @imqueue 3.x ships as native ES modules and requires Node.js
> 22.12 or newer. If you're upgrading from 2.x, see the
> [Migration from 2.x to 3.x](#migration-from-2-x-to-3-x) section below.

## RPC API

The RPC API is a high-level framework for building client–server communication
between services using the Remote Procedure Call pattern. If you want the full
set of @imqueue features, this is the API to build on.

### Configuration

Configuration options are usually passed to a Service or Client constructor. In a
real-world setup, the cleanest approach is to map option values from environment
variables into the object at instantiation, giving you full flexibility at
deployment time. Every service scaffolded from the boilerplate includes a
`config.ts` file where these options can be set, for example:

~~~typescript
export const serviceOptions: Partial<IMQServiceOptions> = {
    cluster: JSON.parse(process.env['REDIS_CLUSTER_CONFIG']),
    safeDelivery: !!process.env['MQ_SAFE_DELIVERY'],
    // etc...
};
~~~

This lets a service be reconfigured by the environment it runs in. For local
deployment you can define environment variables in your shell (global
configuration) or in a `.env` file in the service's root directory (per-service
configuration). On platforms such as AWS you can pass variables in from Parameter
Store, and so on.

#### Generic options (Service and Client)

- **host** — Redis server host. Default: `"localhost"`.
- **port** — Redis server port. Default: `6379`.
- **cluster** — defines a cluster of Redis servers, used instead of the
  host/port pair when provided. It's an array of Redis host/port pairs; @imqueue
  automatically distributes messages across the configured cluster nodes.
- **prefix** — the Redis key prefix used for @imqueue key/value pairs. Default:
  `"imq"`.
- **logger** — a reference to a logger implementation, used by the whole library.
  Default: `console`. The implementation must satisfy the
  [ILogger](https://imqueue.org/api/core/latest/core.ilogger/) interface.
- **safeDelivery** — enables or disables safe message delivery. Default: `false`
  (off). When on, reading a message moves it atomically out of the queue into a
  worker-owned key instead of popping it outright, so a worker that dies *before it
  starts* on a message leaves that message behind to be re-queued rather than
  taking it down with the process. **The guarantee covers that hand-off, not the
  processing:** the worker key is released as soon as the message reaches the
  handler, so a worker killed mid-handler loses it exactly as it would with safe
  delivery off. Delivery is
  [at-least-once in either mode](https://imqueue.org/api/core/latest/core.imqoptions.safedelivery/),
  so handlers should be idempotent, and draining in-flight work before exit is up
  to the application.
- **safeDeliveryTtl** — time to live, in milliseconds, for a message leased by a
  consumer instance — how long it stays in a worker key before the watcher sweeps
  it back onto the main queue. Default: `5000` (5 seconds). This is a **recovery
  deadline for an abandoned hand-off, not a processing deadline**: a slow handler is
  neither interrupted nor re-queued for taking too long, so raising this value
  extends no protection over long-running work. Note it also sets the maintenance
  sweep interval that drives `cleanup`, whether or not safe delivery is on.
- **useGzip** — enables or disables gzip compression. Default: `false` (off).
  @imqueue exchanges messages as plain JSON. If your messages are large,
  compression can be a sensible way to reduce traffic between consumer instances
  and Redis nodes. It trades worker CPU for bandwidth: on the
  [published benchmark](https://imqueue.org/blog/benchmarking-imqueue-throughput/) it cost about 15%
  of throughput and cut a ~1 KB payload by roughly 70%. Both producer and consumer
  of a queue must use the **same** setting — a mismatch makes deserialization fail
  and the message is dropped permanently, even under safe delivery.

#### Client-only options

There are several ways to create and instantiate service clients:

1. Using only pre-generated client source files.
2. Instantiating clients dynamically, with or without generating source files.
3. Implementing clients manually, when you need to.

The following options select among these modes:

- **compile** — allows the generated client module to be interpreted by
  JavaScript on the fly. Default: on, so dynamically generated clients work out
  of the box with no extra configuration. Can be turned off if needed.
- **write** — enables or disables persistence of the generated client code.
  Default: on.
- **path** — where the generated client source files are written. Default:
  `"./src/clients"`.

### Service and Client

IMQ is built so that **you focus only on service development** — the client is
generated automatically from the service's description. Every @imqueue service is
self-describing, so your only responsibilities are writing good descriptions
(correct type definitions and doc-blocks) and, of course, implementing the
functionality itself.

This keeps development simple: you write the service, and the framework handles
the rest.

If you have special requirements, there's nothing stopping you from implementing
clients by hand — but that adds significant work, both to build and, more
importantly, to maintain afterwards.

@imqueue provides the
[IMQService](https://imqueue.org/api/rpc/latest/rpc.imqservice/) and
[IMQClient](https://imqueue.org/api/rpc/latest/rpc.imqclient/) abstract base classes
for concrete implementations to extend.

#### What a generated client looks like

Worth knowing before the examples below, because the shape is not the obvious one.
`imq client generate <ServiceClass> [dir]` writes `<dir>/<ServiceClass>.ts` plus a
compiled `.js`, and that module exports **exactly one symbol**: a namespace named
after the service with a lower-case first letter. Inside it sit the service's
complex types and the client class, whose name is the service's with a trailing
`Service` replaced by `Client`.

So a service class `UserService` is reached like this — and note that the name
passed to the CLI is the service's **class** name, because that is its queue name:

~~~typescript
import { userService } from './clients/UserService.js';

const client = new userService.UserClient({ callTimeout: 5000 });
const data: userService.UserObject = { /* … */ };
~~~

There is no top-level export of `UserClient` or `UserObject`, so importing either
by name will not resolve.

Each service is treated as a package containing at least one service class. A
complex service may consist of several classes, though in microservice terms
that's usually best avoided.

A minimal, empty service looks like this:

~~~typescript
import { IMQService } from '@imqueue/rpc';

export class SomeService extends IMQService {
    // service implementation goes here
}
~~~

A service is an ordinary TypeScript class — it can have private, protected and
public methods, and any properties you like. A few rules must be followed to make
it work correctly:

* Methods that aren't exposed are not accessible remotely. To make a method
  remotely callable, you **must** expose it with the `@expose()` decorator:
  ~~~typescript
  import { IMQService, expose } from '@imqueue/rpc';

  export class SomeService extends IMQService {
    @expose()
    public exposedMethod() { /* implementation... */ }

    public unexposedMethod() { /* implementation... */ }
  }
  ~~~
* Only methods can be exposed. You **cannot** expose class properties.
* To run asynchronous setup during service initialization, override the
  `start()` method:
  ~~~typescript
  import { IMessageQueue, IMQService, expose } from '@imqueue/rpc';

  export class SomeService extends IMQService {
    public async start(): Promise<IMessageQueue | undefined> {
      // async setup here — e.g. open a database connection...
      return super.start();
    }
  }
  ~~~
* Prefer the injected logger over `console` for debug, info, warning and error
  output:
  ~~~typescript
  import { IMQService, expose } from '@imqueue/rpc';

  export class SomeService extends IMQService {
    @expose()
    public loggedStuff() {
      this.logger.log('I am a logged string!');
    }
  }
  ~~~
* Because remote calls send data across the network, all arguments and return
  values of exposed methods **must be JSON-serializable**.
* You **cannot** use the spread operator for exposed-method arguments — it won't
  work on the generated client (a known limitation). Pass such arguments as an
  array instead:
  ~~~typescript
  import { IMQService, expose } from '@imqueue/rpc';

  export class SomeService extends IMQService {
    // INCORRECT — the client for this service will not compile
    @expose()
    public incorrectStuff(...args: any[]) {
      args.forEach((arg) => {
        // do something with arg...
      });
    }

    // CORRECT
    @expose()
    public correctStuff(args: any[]) {
      args.forEach((arg) => {
        // do something with arg...
      });
    }

    // Also fine — this one isn't exposed
    private somePrivateStuff(...args: any[]) {
      args.forEach((arg) => {
        // do something with arg...
      });
    }
  }

  (async () => {
    // assuming we have generated a client for the service — note the namespace,
    // and that `SomeService` yields `SomeClient`, not `SomeServiceClient`:
    const client = new someService.SomeClient();
    await client.start();

    // we'd like to write this:
    client.incorrectStuff(1, 2, 3);
    // but we do this instead — hardly a hardship:
    client.correctStuff([1, 2, 3]);
  })();
  ~~~

### The importance of doc-blocks

JavaScript and TypeScript offer little reflection tooling, so there's no easy way
to recover argument and return-value types at runtime. @imqueue works around this
by reading the doc-blocks attached to your class methods to build its service
description.

This is good practice from every angle: well-written doc-blocks make code more
readable and self-documenting, and let you auto-generate API docs. In @imqueue's
case they're also **mandatory** — the framework needs them to describe a service
and to generate a correctly working client.

Here's what you need to know about writing doc-blocks for @imqueue services:

* Always use `@param` and `@return` tags with a proper type for every input
  argument and return value:

  ~~~typescript
  import { IMQService, expose } from '@imqueue/rpc';

  export class SomeService extends IMQService {
    /**
     * Some method description goes here
     *
     * @param {string} argOne - description of the first argument
     * @param {boolean} argTwo - description of the second argument
     * @param {{name: string, value: number}} [argThree] - description of the third, optional argument
     * @return {{x: number, y: number}} - description of the return value
     */
    @expose()
    public someMethod(
      argOne: string, argTwo: boolean, argThree?: { name: string, value: number },
    ): { x: number, y: number } {
      // do something with the args...
      return { x: 5, y: 7 };
    }
  }
  ~~~
* Wrap optional argument names in `[]` in the doc-block — this is what marks them
  as optional on the generated client.
* Use TypeScript type notation in doc-blocks for arguments and return values;
  it's carried through into the generated client code.
* The documented `@param` list is also what the service's argument-count check
  validates, so it **must** match the method's real arity — a mismatch rejects
  calls with `IMQ_RPC_INVALID_ARGS_COUNT`. An undocumented type falls back to
  `any` rather than failing.
* Compile the service project with **`removeComments: false`**. The doc-blocks
  are read from the emitted sources, so stripping comments leaves the generator
  nothing to work from.

### Complex types

@imqueue services support complex types in the data exchanged between a client
and a service. A few rules ensure that both sides understand and use them
correctly.

Define a complex type's interface on the service side using a **class**, not a
TypeScript interface. This is because a class exists in the compiled JavaScript,
whereas an interface exists only in TypeScript and is gone at runtime. There's a
second wrinkle: JavaScript classes don't support bare properties (only getters
and setters), so while TypeScript is happy with a plain property definition,
@imqueue needs extra metadata to see it at the JavaScript level. That metadata is
supplied by the [`@property()`](https://imqueue.org/api/rpc/latest/rpc.property/) decorator:

~~~typescript
function property(type: string | Thunk | any, isOptional?: boolean): any
~~~

The type is normally a type-definition string, as in the examples below
(`'string'`, `'Address'`, `'Address[]'`). It can also be a constructor, whose
`name` is used, or an anonymous [Thunk](https://imqueue.org/api/rpc/latest/rpc.thunk/) returning
either — which is what a self- or forward-referencing type needs, since a thunk
isn't invoked until the description is first read. The thunk must be anonymous:
a *named* function is taken for a constructor and resolves to its own name.

Note that `isOptional` is **not** inferred from TypeScript's `?` modifier — pass
`true` explicitly, as the examples below do.

Since v3.x, each complex-type class **must** also be annotated with the
`@classType()` class decorator. @imqueue v3 uses standard (TC39) decorators, under
which `@property()` only collects field metadata — the class-level `@classType()`
then finalizes and registers that metadata as a named type, so that both the
service and the generated client recognise it:

~~~typescript
function classType(): any
~~~

Omitting it produces no error — the type is simply missing from the RPC type
description, and generated clients then reference an undeclared type.
[`@indexed()`](https://imqueue.org/api/rpc/latest/rpc.indexed/) performs the same flush in addition
to recording an index signature, so a class carrying `@indexed()` does not also
need `@classType()`.

Putting it together:

~~~typescript
// service-side definition of the type:
import { classType, property } from '@imqueue/rpc';

@classType()
class UserObject {
    @property('string')
    firstName: string;

    @property('string')
    lastName: string;

    @property('string')
    email: string;

    @property('string', true)
    phoneNumber?: string;
}
~~~

This compiles, on the client side, to a matching TypeScript interface, so the
type can be used for correct type-checking on both sides:

~~~typescript
// generated client-side definition of the type
interface UserObject {
    firstName: string;
    lastName: string;
    email: string;
    phoneNumber?: string;
}
~~~

The type can then be used in service methods (assuming the definition lives in
its own file):

~~~typescript
import { IMQService, expose } from '@imqueue/rpc';
import { UserObject } from './types/UserObject';

class UserService extends IMQService {
    /**
     * Updates a user record
     *
     * @param {UserObject} data - user data fields
     * @return {Promise<UserObject | null>} - the saved user, or null on failure
     */
    @expose()
    public async update(data: UserObject): Promise<UserObject | null> {
        // do logic...
        return data;
    }
}
~~~

This guarantees correct type-checking at the client level.

Complex types can nest other complex types. Suppose we extend the user model to
be associated with one or more address objects:

~~~typescript
import { classType, property } from '@imqueue/rpc';

@classType()
class AddressObject {
    @property('string')
    country: string;

    @property('string')
    city: string;

    @property('string')
    address: string;

    @property('string', true)
    phoneNumber?: string;
}

@classType()
class UserObject {
    @property('string')
    firstName: string;

    @property('string')
    lastName: string;

    @property('string')
    email: string;

    @property('AddressObject[]', true)
    addresses?: AddressObject[];
}
~~~

### Working with the service description

If you ever need to access a service's description metadata, just call
`describe()` on a service client:

~~~typescript
// assuming this runs in an async context:
import { userService } from './clients/UserService.js';

const client = new userService.UserClient();
await client.start();
console.log(await client.describe());
~~~

This prints all the metadata about the service's classes, methods and complex
types.

### Delayed messaging

Delayed messaging with `@imqueue/rpc` is easy: any exposed service method can be
called with a delay. This is handy for building scheduling queues. The generator
appends two optional trailing parameters to every generated client method — call
metadata (of type [IMQMetadata](https://imqueue.org/api/rpc/latest/rpc.imqmetadata/)) first, then
the delay (of type [IMQDelay](https://imqueue.org/api/rpc/latest/rpc.imqdelay/)):

~~~typescript
import { IMQDelay, IMQMetadata } from '@imqueue/rpc';
import { userService } from './clients/UserService.js';

const client = new userService.UserClient();
const data: userService.UserObject = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'john@doe.com',
};

await client.start();
// run the scheduled work in 1 hour, and handle the result asynchronously
// once it completes, without blocking:
client
    .doScheduledStuff(
        data,
        undefined,            // metadata slot — skipped
        new IMQDelay(1, 'h'), // delay is always last
    )
    .then((result: any) => client.logger.log(result));
~~~

Both trailing parameters are optional, and an ordinary call passes neither. When
all you want is a delay, skip the metadata slot with `undefined` as above: from
**3.4.0** a trailing `undefined` on a delayed call is a placeholder and is never
delivered to the service. Pass a real
[IMQMetadata](https://imqueue.org/api/rpc/latest/rpc.imqmetadata/) bag there when you have one —
tracing context, an audit reason — and the delay still goes last.

The one form to avoid is the delay dropped straight into the metadata slot. The
client strips these two parameters **by identity, not by position**, so it works
at runtime — but it does not type-check without a cast, and the cast is what
makes the placeholder rules below observable. Keep the delay last instead.

On older versions the placeholder behaves differently. **3.3.1** dropped only one
trailing `undefined`, and only when no metadata was passed, so skipping an
optional declared argument as well still delivered it — as `null`, which means a
defaulted parameter did not fall back to its default. **3.3.0 and earlier**
delivered the placeholder itself, and a method whose declared parameters are all
required rejected the call with `IMQ_RPC_INVALID_ARGS_COUNT`. An undelayed call
drops nothing, on every version.

For the patterns this enables — one-shot reminders, sweepers, backoff — see
[delayed and scheduled work without a job
system](https://imqueue.org/blog/scheduled-work-without-a-job-system/).

### Reading call metadata

Whatever a caller puts in the metadata bag travels with the request, and the
service reads it without threading it through its own method signatures.
`IMQService` binds every in-flight request to the async execution context, so
[currentMetadata()](https://imqueue.org/api/rpc/latest/rpc.currentmetadata/) works inside any
exposed method — and inside anything that method awaits:

~~~typescript
import { IMQService, expose, currentMetadata } from '@imqueue/rpc';

class OrderService extends IMQService {
    /**
     * Ships the order with the given identifier
     *
     * @param {string} orderId - order identifier
     * @return {Promise<void>}
     */
    @expose()
    public async ship(orderId: string): Promise<void> {
        const meta = currentMetadata();

        this.logger.info('shipping', orderId, meta?.reason);
    }
}
~~~

The bag is opaque, and it arrives as plain JSON — read its fields directly
rather than expecting an `IMQMetadata` instance. Outside of a request, at
start-up for instance, `currentMetadata()` returns `undefined`. Don't declare an
`IMQMetadata` parameter on an exposed method to receive it: the generator treats
a trailing `IMQMetadata` or `IMQDelay` parameter as its own and strips it from
the client it builds.

### Locking

Locking is a powerful tool in `@imqueue/rpc` for optimising remote calls.
Imagine one of your service processes receives hundreds or thousands of calls to
the same method, with the same execution context, in a very short window — and each
returns the same result. This happens, for example, when popular content is requested by many
users but stays effectively static over that short period. Without help, your
back-end runs the same work hundreds or thousands of times a second for no real
reason. Locks fix this.

When `@lock()` wraps a method, the first matching call acquires an asynchronous
lock. Until that call resolves, all other calls with the same signature wait; when
it resolves, they're all resolved with the same result.

> **These are not distributed locks.** The lock lives in the memory of one Node.js
> process, so the coalescing described here happens *per process*. Separate
> processes, `multiProcess` cluster workers and service replicas each maintain their
> own independent locks and will run the guarded code concurrently — four replicas
> under the same burst do the work four times, not once. That is usually still a
> large win, but if you need genuine mutual exclusion across replicas, use a Redis-
> or database-backed lock. See
> [IMQLock](https://imqueue.org/api/rpc/latest/rpc.imqlock/) for the precise scope.

For example, if a specific blog post is fetched 100 times over 100 milliseconds,
and the database fetch itself takes about 100 milliseconds, the actual logic runs
just once — and all 100 waiting clients are resolved with the same value:

~~~typescript
import { IMQService, expose, lock } from '@imqueue/rpc';
import { BlogPost } from './types';

class BlogService extends IMQService {
    /**
     * Returns the blog post for a given identifier
     *
     * @param {string} id - blog post identifier
     * @return {Promise<BlogPost>} - the blog post data
     */
    @lock()
    @expose()
    public async fetchPost(id: string): Promise<BlogPost> {
        let data: BlogPost;
        // fetch the blog post from the database by id...
        return data;
    }
}
~~~

Meanwhile, concurrent calls for *different* blog post identifiers run under their
own contextual locks and resolve their clients with their own values.

This offers two advantages:

1. **Lower back-end load** — less logic executed, fewer database calls.
2. **Better average response time** — the first caller waits longest, but the
   last is served almost instantly, so the average across all callers improves.

There is a trade-off. To identify the execution context, the locking mechanism
hashes the call's signature, which costs time and CPU. The algorithm is efficient,
but its cost grows with the length of the method signature. In some cases you may
gain nothing from locking, so weigh it up: for high-load, slow methods it's
clearly worth it; when the method is cheaper to run than its signature is to hash,
it isn't.

Locking isn't limited to a method decorator. IMQ also provides a general-purpose
asynchronous lock class,
[IMQLock](https://imqueue.org/api/rpc/latest/rpc.imqlock/), that you can use
wherever you need it across your back-end.

### Caching

Caching is another optimisation tool @imqueue provides. It caches a method's
results using a caching adapter (Redis is the default, and currently the only
built-in one). You can supply your own adapter by implementing the
[ICache](https://imqueue.org/api/rpc/latest/rpc.icache/) and
[ICacheConstructor](https://imqueue.org/api/rpc/latest/rpc.icacheconstructor/)
interfaces.

Out of the box, use the `@cache()` decorator on service methods, or work with the
[IMQCache](https://imqueue.org/api/rpc/latest/rpc.imqcache/) registry and the
[RedisCache](https://imqueue.org/api/rpc/latest/rpc.rediscache/) engine directly.

Typical usage:

~~~typescript
import { IMQService, expose, cache } from '@imqueue/rpc';
import { BlogPost } from './types';

class BlogService extends IMQService {
    /**
     * Returns the blog post for a given identifier
     *
     * @param {string} id - blog post identifier
     * @return {Promise<BlogPost>} - the blog post data
     */
    @cache()
    @expose()
    public async fetchPost(id: string): Promise<BlogPost> {
        let data: BlogPost;
        // do stuff...
        return data;
    }
}
~~~

Like `@lock()`, `@cache()` works per call signature. All of @imqueue's decorators
can be combined on a service method to improve overall performance and stability.
Write load tests for your back-end and use them to find the best optimisation
strategy for your case.

## Messaging API

The Messaging API is the low-level API implementing the Message Queue pattern
used for inter-service communication. Reach for it when you need **only** the
messaging layer in your code.

This API concerns the messaging-engine adapter, its configuration, logging
injection and profiling.

### The IMQ factory and adapters

The [IMQ](https://imqueue.org/api/core/latest/core.imq/) factory constructs
message-queue instances. Currently IMQ ships with a Redis adapter out of the box.
Prefer creating instances through the factory rather than constructing a queue
class yourself: it picks the right implementation for the options you pass —
supplying [cluster](https://imqueue.org/api/core/latest/core.imqoptions.cluster/) or
[clusterManagers](https://imqueue.org/api/core/latest/core.imqoptions.clustermanagers/) gets you a
[ClusteredRedisQueue](https://imqueue.org/api/core/latest/core.clusteredredisqueue/) instead of a
[RedisQueue](https://imqueue.org/api/core/latest/core.redisqueue/), with no change at the call site.

`IMQ` is the **default** export of `@imqueue/core`, so import it without braces.
Note that `export *` never forwards a default: it is the one part of the core
surface `@imqueue/rpc` does not re-export, so import it from `@imqueue/core`
directly.

Example:

~~~typescript
import IMQ from '@imqueue/core';

const mq = IMQ.create('MyMQ', { vendor: 'Redis' });
~~~

You don't need to specify the vendor —
[`'Redis'`](https://imqueue.org/api/core/latest/core.imqoptions.vendor/) is the default, and
currently the only supported value; [IMQ.create()](https://imqueue.org/api/core/latest/core.imq.create/)
throws a `TypeError` for anything else. The factory builds only the adapters the
framework ships with, so a queue of your own is instantiated directly rather than
through it:

~~~typescript
import { MyMQAdapter } from './path/to/MyMQAdapter.js';

const mq = new MyMQAdapter('MyMQ');
~~~

Any such adapter must implement the
[IMessageQueue](https://imqueue.org/api/core/latest/core.imessagequeue/)
interface, extending `EventEmitter` and emitting `'message'` and `'error'`
events.

The factory performs no I/O, so the queue it returns is not connected — call
`start()` on it, or `send()`, which starts the queue implicitly.

### Redis Queue

[RedisQueue](https://imqueue.org/api/core/latest/core.redisqueue/) is the core
Redis-based message-queue implementation, providing the engine for a single Redis
node.

### Clustered Redis Queue

[ClusteredRedisQueue](https://imqueue.org/api/core/latest/core.clusteredredisqueue/)
extends `RedisQueue` to work across a cluster of Redis nodes, with automatic
round-robin load balancing between them.

## Profiling and debugging

Profiling and debugging matter throughout the development and ongoing support of
any system.

@imqueue provides a simple, built-in tool for measuring and debugging service
method execution: the `@profile()` decorator. Apply it to the parts of the system
you most need to keep an eye on.

Profiled timing is reported in **microseconds** by default.

Usage:

~~~typescript
import { IMQService, expose, profile } from '@imqueue/rpc';

class MonitoredService extends IMQService {
    @profile()
    @expose()
    public exposedStuff() {
        // call some internals:
        this.internalStuff(1, 2, 3);
        // do anything else...
    }

    @profile()
    private internalStuff(...args: any[]) {
        for (let i = 0; i < 100000; i++) {
        }
    }

    @profile({ enableDebugTime: true })
    private forcedTimeProfiling(...args: any[]) {

    }

    @profile({ enableDebugArgs: true })
    private forcedArgsProfiling(...args: any[]) {

    }

    @profile({ enableDebugTime: true, enableDebugArgs: true })
    private forcedFullProfiling(...args: any[]) {

    }
}
~~~

Called with no arguments, `@profile()` follows the environment configuration,
which can turn profiling on or off. A
[ProfileDecoratorOptions](https://imqueue.org/api/core/latest/core.profiledecoratoroptions/) object
overrides the environment and forces time and/or argument profiling explicitly —
but only for fields passed as real booleans; any other value is ignored and the
environment default applies.

Whether timing and argument logging are enabled is resolved **once, when the
class is defined**, so changing `process.env` later has no effect.

We recommend managing profiling state through `.env` files (per service) or by
setting the variables globally (for the whole environment). Those variables are:

- `IMQ_LOG_TIME=1|0` — enables or disables execution-time profiling. Empty is
  treated as `0`, the default.
- `IMQ_LOG_ARGS=1|0` — enables or disables argument debug logging. Empty is
  treated as `0`, the default.
- `IMQ_LOG_TIME_FORMAT="microseconds"|"milliseconds"|"seconds"` — sets the time
  format in the debug output. Empty is treated as `"microseconds"`, the default.

`@profile()` writes through the `logger` property of **the instance it decorates**
— any [ILogger](https://imqueue.org/api/core/latest/core.ilogger/). Inside a service class that
property is already there, so the decorator needs no extra setup. It also works on
any class method, not just service classes, but then the logger is yours to
provide: an instance with no `logger` profiles the method and produces no output
at all, with no warning. Static methods are never logged, because the logger is
looked up on instances only.

~~~typescript
import { profile } from '@imqueue/core';

class SomeClass {
    // the decorator logs through this property only;
    // without a logger nothing is ever written
    public logger = console;

    @profile()
    protected someProtectedMethod() {
    }
}
~~~

Note that enabling profiling can slightly reduce overall back-end performance —
but it's invaluable for diagnosing and eliminating bottlenecks and slow code
paths.

## Migration from 2.x to 3.x

Version 3.x is a modernization release of `@imqueue/core` and `@imqueue/rpc`.
It moves the packages to native ES modules and standard TypeScript decorators,
built with TypeScript 7. The public runtime API is largely the same, but the
following changes require attention when upgrading from 2.x.

### ES modules and Node

Both packages are now published as **native ES modules** (`"type": "module"`)
and require **Node.js 22.12 or newer**. In practice this means:

- Import `@imqueue/core` / `@imqueue/rpc` from ESM code; `require()` of these
  packages is no longer supported.
- In your own project, use ESM as well and add the `.js` extension to relative
  import specifiers (Node's `nodenext` resolution), for example
  `import { UserObject } from './types/UserObject.js';`.

### Standard decorators and tsconfig

@imqueue 3.x uses **standard (TC39) decorators** instead of the legacy
experimental implementation. Update your `tsconfig.json` accordingly:

~~~jsonc
{
  "compilerOptions": {
    // remove these — legacy decorators are no longer used:
    // "experimentalDecorators": true,
    // "emitDecoratorMetadata": true,

    // use a modern target and the standard-decorators metadata lib:
    "target": "es2024",
    "lib": ["es2024", "esnext.decorators"],
    "module": "nodenext",
    "moduleResolution": "nodenext",

    // keep this: doc-blocks are the only type source the client
    // generator reads, so stripping comments leaves it nothing
    "removeComments": false
  }
}
~~~

### `@classType()` is now required on complex types

Under standard decorators, `@property()` only collects field metadata — it no
longer registers the class itself. Every complex type must now be annotated with
the new `@classType()` class decorator (see
[Complex Types](#complex-types)). Add it to each `@property()`-decorated class:

~~~typescript
// 2.x
import { property } from '@imqueue/rpc';

class UserObject {
    @property('string')
    firstName: string;
}

// 3.x
import { classType, property } from '@imqueue/rpc';

@classType()
class UserObject {
    @property('string')
    firstName: string;
}
~~~

### Removed helpers

A number of general-purpose utilities that these packages exported alongside
their real API have been removed. Most had a standard-library equivalent by the
time 3.x was cut; the rest were internals that were never meant to be public.

Removed from `@imqueue/core` (and therefore from `@imqueue/rpc`, which
re-exports it):

| Removed export | Replacement |
| --- | --- |
| `uuid()` | `randomUUID()` from `node:crypto` |
| `promisify()` | `promisify` from `node:util` |
| `sha1()` | `createHash('sha1')` from `node:crypto` |
| `IJson` | `JsonObject` — `IJson` was only ever an alias for it |
| `intrand()` | no equivalent — inline your own random-integer helper |
| `propertiesOf()` | no equivalent — walk the prototype chain yourself if you need it |
| `pack()` / `unpack()` | internal message codec — removed; `useGzip` covers compression on the wire |
| `buildOptions()` | internal helper — removed, inline your own option merge |
| `copyEventEmitter()` | internal helper — removed |

Removed from `@imqueue/rpc`:

| Removed export | Replacement |
| --- | --- |
| `fileExists()` / `mkdir()` / `writeFile()` | `node:fs/promises` |
| `osUuid()` | no equivalent — it returned a machine UUID; use `randomUUID()` if a per-process id will do |
| `signature()` | internal — the call-signature hash behind `@lock()` and `@cache()` |
| `pid()` / `forgetPid()` / `IMQ_PID_DIR` / `IMQ_TMP_DIR` | internal PID-file bookkeeping — removed |
| `SIGNALS` | internal — see [IMQOptions.handleSignals](https://imqueue.org/api/core/latest/core.imqoptions.handlesignals/) |

For example:

~~~typescript
// 2.x
import { uuid } from '@imqueue/core';
const id = uuid();

// 3.x
import { randomUUID } from 'node:crypto';
const id = randomUUID();
~~~

The rest of the runtime API — `IMQ`, `RedisQueue`, `ClusteredRedisQueue`,
`profile`, `IMQService`, `IMQClient`, and the `@expose()` / `@lock()` /
`@cache()` / `@property()` decorators — is unchanged, `@classType()` above being
the one addition you must make.

