pg-cache package

PostgreSQL-managed cache on Redis for @imqueue service methods: results are memoised, and PostgreSQL itself says when to drop them.

Decorate the service class with PgCache(), then mark cached methods with cacheWith() or cacheBy() to declare which tables they depend on.

Remarks

The point is invalidation that is neither a guessed TTL nor a manual del() call. PgCache() installs a change-notify trigger on each declared table and subscribes to one LISTEN/NOTIFY channel per table; when a row changes, the entries tagged with that table are dropped. So an entry lives exactly as long as the data behind it is unchanged.

Two things to know. The triggers and the subscription are established in start(), so a service that never starts is never cached. And a ChannelFilter given as an array of ChannelOperation is an EXCLUSION list — the operations named in it do not invalidate — which reads the opposite way round from how it looks.

Example

import { PgCache, cacheWith } from '@imqueue/pg-cache';

@PgCache({
    postgres: process.env.DB_URL!,
    redis: { host: 'localhost', port: 6379 },
})
class UserService extends IMQService {
    @cacheWith({ channels: ['users'] })
    public async list(): Promise<User[]> {
        return this.db.query('SELECT * FROM users');
    }
}

Enumerations

Enumeration

Description

ChannelOperation

The row-level operation that produced a change notification. Matches the PostgreSQL trigger's TG_OP.

Functions

Function

Description

cacheBy(model, options)

Decorator factory @cacheBy(Model, CacheByOptions) This decorator should be used on a service methods, to set the caching rules for a method. Caching rules within this decorator are defined by a passed model, which is treated as a root model of the call and it analyzes cache invalidation based on passed runtime fields arguments, which prevents unnecessary cache invalidations. So it is more intellectual way to invalidate cache instead of any changes on described list of tables.

cacheWith(options)

Decorator factory @cacheWith(CacheWithOptions) This decorator should be used on a service methods, to set the caching rules for a method.

channelsOf(model, fields, tables)

Retrieves table names as channels from the given model and filter them by a given fields map, if passed. Returns result as list of table names.

declaringPrototype(instance, methodName)

Walks up from a constructed instance to the prototype that actually declares the given method, mirroring legacy decoration where the decorator target is the declaring prototype. Falls back to the instance's own prototype.

envBool(name, defaultValue)

Reads a boolean environment variable, accepting the human-friendly spellings 1/true/yes/on and 0/false/no/off (case-insensitive). The previous !!+value idiom parsed values like true as NaN, i.e. false.

fetchError(logger, err, key, decorator)

Reports a failed cache read at warning level. The caller then falls through to the real method, so a read failure costs latency rather than correctness.

initError(logger, className, methodName, decorator)

Reports that a cached method ran before the cache existed — the service was decorated but start() has not completed, so there is nothing to read or write. The method still executes; it is simply not cached.

isStandardDecorator(context)

Returns true if the decorator was invoked in standard (TC39) mode, i.e. its second argument is a decorator context object carrying a kind.

makeChannel(name, method, options)

Makes channel entry from a given channel name, class method name and options.

PgCache(options)

Class decorator turning an @imqueue service into a PostgreSQL-invalidated cache: method results are memoised in redis, and PostgreSQL itself tells the service when to drop them.

It installs a change-notify trigger on every table the service's cacheWith() and cacheBy() decorators declare a dependency on, and subscribes to one LISTEN/NOTIFY channel per table. When a row changes, the matching cached results are invalidated by tag — so a cache entry lives exactly as long as the data behind it is unchanged, rather than for a guessed TTL.

import { PgCache, cacheWith } from '@imqueue/pg-cache';

@PgCache({
    postgres: process.env.DB_URL!,
    redis: { host: 'localhost', port: 6379 },
})
class UserService extends IMQService {
    @cacheWith({ channels: ['users'] })
    public async list(): Promise<User[]> { ... }
}

Applied to the class, it wraps start(): the subscription and the triggers are established there, after any existing start() implementation has run. So the cache is inert until the service is started, and a service that never calls start() is never cached.

Works both as a standard (TC39) decorator and as a legacy (experimentalDecorators) one, matching @imqueue/rpc, so it can be applied in either compilation mode.

Redis is resolved in order: options.redisCache, then options.redis, then a cache property already on the service. If none is available start() throws.

registerChannelsOnce(proto, methodName, register)

Registers pg-cache channel entries for a method on the given prototype exactly once, even when called from a per-construction initializer.

setError(logger, err, key, decorator)

Reports a failed cache write at warning level. Always logs: a write failure matters even when tracing is off.

setInfo(logger, res, key, decorator)

Reports a successful cache write and passes the value straight through, so it can be used inline in a return position. Logs only when PG_CACHE_DEBUG is on.

Interfaces

Interface

Description

CacheByOptions

Options expected by @cacheBy() decorator factory

CacheWithOptions

Options for the cacheWith() method decorator: which tables invalidate the cached result, how long it may live, and the tag it is stored under.

ChannelPayload

Payload delivered on a table's notification channel by the installed trigger, describing a single row change.

FilteredChannels

Map of table name to the filter that decides which of its changes matter, for method decorators that watch several tables with different rules.

ILogger

Minimal logger interface accepted by this package. Structurally compatible with the console object and with @imqueue loggers, so any of them can be passed without depending on @imqueue/core.

PgCacheable

What the PgCache() decorator adds to the class it is applied to. A decorated service gains these three members, so code inside the service can reach the cache and the subscription directly.

PgCacheChannels

Registry of cached methods keyed by the PostgreSQL notification channel that invalidates them. The key is a table name: the installed trigger uses the table name as its NOTIFY channel, so the two are the same string.

PgCacheOptions

Options for the PgCache() class decorator: where PostgreSQL and redis live, and how the change-notify triggers behave.

Exactly one of redis or redisCache must be supplied — redis to let the decorator build its own connection, redisCache to reuse one the service already owns.

Variables

Variable

Description

DEFAULT_CACHE_TTL

Default lifetime of a cached entry, in milliseconds — 24 hours.

A TTL is a backstop, not the primary invalidation mechanism: entries are normally dropped by a PostgreSQL change notification long before it expires. It exists so an entry cannot outlive its data indefinitely if a notification is ever missed.

PG_CACHE_DEBUG

Whether verbose cache tracing is on, read once from the PG_CACHE_DEBUG environment variable at import time.

When enabled, cache saves, fetches and trigger installation are logged at info level. Warnings are logged regardless. Because it is read at import time, changing the variable afterwards has no effect.

PG_CACHE_TRIGGER

Default PL/pgSQL trigger function installed on every watched table.

It builds a JSON payload of the changed row and issues PG_NOTIFY on a channel named after the table. The payload shape is ChannelPayload: timestamp, operation, schema, table and the row itself — NEW for inserts and updates, OLD for deletes.

Column values are read out of information_schema and cast to TEXT, so every field arrives as a string regardless of its SQL type.

Note PostgreSQL caps a NOTIFY payload at 8000 bytes; a change to a very wide row can exceed that and the notification will be rejected. Override with PgCacheOptions.triggerDefinition if the default does not suit — see PgCacheOptions.

Type Aliases

Type Alias

Description

ChannelFilter

Narrows which changes to a table invalidate a cached method.

The two forms behave in OPPOSITE directions, which is easy to get wrong:

  • A ChannelOperation array is an **exclusion** list. Operations named in it do NOT invalidate; everything else does. So [ChannelOperation.DELETE] means "invalidate on inserts and updates, ignore deletes" — not "invalidate on deletes". - A ChannelPayloadFilter is an **inclusion** predicate: it invalidates when it returns true.

Omitting the filter invalidates on every change to the table.

ChannelPayloadFilter

Predicate deciding whether one change should invalidate the cached method.

Returning true invalidates. Unlike the array form of ChannelFilter, this reads the way you expect — see that type for the inversion.

ClassDecorator

A dual-mode class decorator: called as (constructor) by legacy (experimentalDecorators) TypeScript and as (value, context) by standard (TC39) decorators. In both forms the first argument is the class, and the result is the class augmented with PgCacheable.

Supporting both is what lets this package decorate @imqueue services compiled in either mode, the same way @imqueue/rpc and @imqueue/core decorators do.

MethodDecorator

A dual-mode method decorator: called as (target, propertyKey, descriptor) by legacy (experimentalDecorators) TypeScript and as (value, context) by standard (TC39) decorators.

Use isStandardDecorator() on the second argument to tell the two apart.

PgCacheChannel

One registered dependency of a cached method: the method to invalidate, and an optional filter narrowing which changes should trigger it.

Position 0 is the decorated method name; position 1 is the filter, or undefined to invalidate on every change to the table.

Read this page as plain markdown — no HTML, no navigation. For pasting into an LLM, or for an agent to fetch.