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

aragami

v1.4.0

Published

Redis ORM / Utility with matching in-memory implementations for caching, locks, queues, Pub/Sub and RxJS election.

Readme

Aragami

English · 简体中文

Aragami is a Redis ORM / Utility for TypeScript, with corresponding in-memory implementations.

It brings two parts of application development together: storing typed models and coordinating work around them. A class describes the value, its metadata identifies the resource, and Aragami handles serialization, class restoration and the Redis operations behind caching, locks, queues, broadcasting and election.

The same model can be cached by user ID, protected during an update, or published as a message. Each capability has its own storage namespace, while the application keeps the same class namespace + your own key design.

| What your application needs | What Aragami provides | | --- | --- | | Reuse a loaded profile or computed result | Typed caching, TTL and cache-aside loading | | Coordinate access to shared resources | Exclusive locks and read/write locks | | Move work into background consumers | Acknowledged queues and continuous workers | | Make work available at a later time | Delayed jobs in the same queue | | Notify every interested subscriber | Live Pub/Sub, with callback and RxJS APIs | | Choose one or several active coordinators | Election snapshots and leader selection |

Name & philosophy

Aragami takes its name from Aragami, a song by the composer xi.

🎵 Aragami / xi — xi Official on YouTube

The library keeps resource identity close to the model, and provides the same operations as your application moves from local memory to shared Redis state.

Getting started

Install the package in your application:

npm install aragami

Create an Aragami instance with a Redis connection to share state across application processes:

import { Aragami } from 'aragami';

const aragami = new Aragami({
  redis: { uri: 'redis://127.0.0.1:6379' },
});

Calling new Aragami() instead uses memory. This is useful for local development and tests that need the same public APIs without a Redis server. The scope of shared state differs between the backends; the driver section explains those boundaries.

The package provides CommonJS, native ESM and TypeScript declarations, and loads reflect-metadata through its entry point. For the decorator examples below, enable experimentalDecorators and emitDecoratorMetadata in TypeScript's compilerOptions.

Redis mode supports Redis 5.0.0 or newer, with Lua scripting and Pub/Sub enabled. The complete Redis suite and built CJS/native ESM packages have been verified against 5.0.0 and 7.4.9. Queue, read/write lock and election scripts combine Redis TIME with writes, relying on script effects replication, enabled by default since Redis 5.0.

The Rx APIs use RxJS ^7.8.2, compatible with the ^7.1.0 peer range declared by NestJS 12.

One model, two key layers

Aragami organizes resources around classes. The first key layer is the class namespace: @CachePrefix supplies it, or the class name is used. The second layer identifies a particular value, queue, topic or resource within that namespace.

For example, a user model can belong to users, with its ID supplying the individual key:

import { CachePrefix, CacheKey, LockKey } from 'aragami';

@CachePrefix('users')
class User {
  @CacheKey()
  @LockKey((id) => id)
  id!: string;
  name!: string;
}

Here, CacheKey gives cache entries their identity, and LockKey identifies the resource used by locking and election. This example makes both use the ID itself. Without a factory, @LockKey() produces fieldName_fieldValue, such as id_u1. Key decorators also work with methods, getters and asynchronous factories.

An explicit key overrides CacheKey. Queue insertion and message publication use CacheKey when available, then fall back to default; queue consumer/query methods and message subscriptions use the supplied key, or the literal default. Locks also accept strings as direct resource keys. Election accepts a string, an object, or no argument; objects without LockKey use their class namespace plus default.

Cache entries, exclusive locks, read/write locks, queues, Pub/Sub and election remain independent even when their logical keys match. You can use the same model across these features without merging their state. The examples below reuse aragami and User from this introduction.

Typed caching and class restoration

A cache saves values that are expensive to load or calculate. Aragami adds a model layer to that operation: reading a cached User restores a User instance, so application code can keep working with its class rather than manually rebuilding it after every read.

Use set to store an instance or a class plus an object, and get to load by class and key:

const user = await aragami.set(User, { id: 'u1', name: 'Alice' });
const cached = await aragami.get(User, 'u1');
await aragami.set(user, { ttl: 30_000 });

The returned values are model instances; a missing cache entry returns undefined. @CastType(() => Child) describes nested models, including array elements, and @CastType(() => Date) restores date fields. This mapping between classes, keys and serialized values is the ORM part of Aragami.

TTL controls how long a value remains cached, in milliseconds. An explicit ttl takes precedence over the model's @CacheTTL, then defaultTTL; the final default is 0, meaning no expiry. Explicit keys, including an empty string, override model metadata.

For inspection and cleanup, has checks existence, del removes an entry, and keys, values and entries enumerate a class namespace. clear removes entries from that namespace. The listing and clearing methods accept a literal key prefix, making it possible to work with one family of cached values.

Load on a cache miss

Cache-aside loading combines lookup and population. cache returns the cached model when present; otherwise it calls your loader and stores the result. This fits operations such as fetching a profile from a database or another service:

const profile = await aragami.cache(User, 'u1', () => fetchUser('u1'));

An empty or missing source key bypasses caching, and null/undefined loader results are not stored. useCache wraps a loader with the same behavior when you want a reusable function rather than a separate cache call at each use.

Exclusive locks

An exclusive lock lets one operation at a time work on a resource. It is useful when two requests might update the same user, or when several processes need to coordinate a shared operation. Aragami derives object resources from LockKey, so the identity can stay next to the model.

The callback runs after the lock is acquired, and its completion releases the lock:

await aragami.lock(user, () => saveUser(user));
const free = await aragami.isFree(user);

You can also pass a string key or an array of objects and strings. A multi-resource lock deduplicates repeated keys and runs immediately for an empty array. Synchronous and asynchronous callbacks preserve their return values and exceptions. useLock applies the same behavior to a reusable function.

Redis locks use leases that renew while the callback is running, with release notifications and polling to wake waiters. Acquisition timeouts throw LockAcquireTimeoutError. A long process pause or Redis outage can still expire a lease; the callback is not forcibly cancelled, and external writes are not fenced.

lock.multiKeyBypass defaults to false. Enable it for local all-or-nothing acquisition so a request waiting for B does not reserve A. redis.lock.multiKeyBypass overrides the common setting. With Redis stacked enabled, bypass limits local coordination to individual acquisition attempts; Redis itself always acquires all keys atomically. Ordinary and read/write locks retain independent state.

Read/write locks

A read/write lock allows several readers to share a resource while keeping writers exclusive. Use it when reads can safely overlap, but a change must wait for current readers and prevent overlapping reads or writes while it runs.

The first argument is RWLockMode, an exported TypeScript enum with READ and WRITE members:

import { RWLockMode } from 'aragami';

await aragami.rwLock(RWLockMode.READ, user, () => readUser(user));
await aragami.rwLock(RWLockMode.WRITE, user, () => saveUser(user));

Read/write locks use the same object metadata and multi-resource rules as exclusive locks, but have an independent physical namespace and configuration. isRWFree(mode, resource) checks whether that mode can enter; useRWLock wraps a function.

The default strategy is RWLockPriority.WRITE_FIRST. Choose READ_FIRST to favor waiting readers, or FIFO to order conflicting requests by first registration. Readers can overlap, unrelated keys can proceed, and held locks are never preempted. The deprecated writerPriority setting is only consulted when writerPriorityStrategy is absent.

import { Aragami, RWLockPriority, FifoRWLockPriorityStrategy } from 'aragami';

const ordered = new Aragami({
  rwLock: { writerPriorityStrategy: RWLockPriority.FIFO },
});
const samePolicy = new Aragami({
  rwLock: { writerPriorityStrategy: new FifoRWLockPriorityStrategy() },
});

ReadFirstRWLockPriorityStrategy, WriteFirstRWLockPriorityStrategy and FifoRWLockPriorityStrategy are exported drivers. They score readers/writers as 1/0, 0/1 and 0/0 respectively. Higher scores enter first; equal scores use FIFO for conflicting requests.

rwLock.multiKeyBypass defaults to false: a waiting request for [A, B] can also block A while B is held. Enable it to let A's requests pass when the earlier waiter is blocked by an incompatible holder on B. This exception only checks actual holders on keys outside the candidate's resources. isRWFree observes the same rules with a prospective request of age zero.

Custom drivers use a synchronous pure function, compiled to Redis-compatible Lua when the instance is constructed:

import { RWLockPriorityStrategy } from 'aragami';

class AgingWriterStrategy
  extends RWLockPriorityStrategy<{ promoteAfterMs: number }> {
  constructor(promoteAfterMs = 30_000) {
    super((request, context) => {
      if (request.mode === 'WRITE' &&
          request.waitedMs >= context.params.promoteAfterMs) return 2;
      return request.mode === 'READ' ? 1 : 0;
    }, { promoteAfterMs });
  }
}

const app = new Aragami({
  rwLock: {
    writerPriorityStrategy: new AgingWriterStrategy(),
    multiKeyBypass: true,
  },
});

The function receives a read-only request (id, mode, deduplicated logical keys, enqueuedAt, waitedMs, sequence) and context (now, the complete ordered waiting snapshot including the current request, and fixed params). Times are milliseconds. It must return a finite number; negative and fractional scores are valid, and zero is a priority rather than a rejection or delay. getPriority(request, state) evaluates the same function locally. params, luaSource and fingerprint are read-only; reuse the instance without recompilation.

Functions cannot capture external variables, use this, imports, async code, I/O, clocks or randomness. Pass configuration as a flat record of finite numbers, strings and booleans. Use explicit boolean conditions and strict equality so JavaScript and Lua agree. Local computations and helpers whose types can be inferred from the inputs are supported. Invalid functions fail at construction; invalid scores fail acquisition. Custom waiters are reevaluated at most every 200 milliseconds in both backends. Active requests sharing a Redis rw namespace must use the same strategy and bypass setting.

The callback receives a manager for changing existing resources:

await app.rwLock(RWLockMode.READ, ['user', 'quota'], async (manager) => {
  await readUser();
  const upgrade = await manager.upgradeWrite('user');
  if (upgrade.intervenedKeys.includes('user')) await readUser();
  await saveUser();
  await manager.downgradeRead('user');
});

upgradeWrite(keys?) and downgradeRead(keys?) apply to all callback resources when keys are omitted, or a selected subset otherwise; new resources cannot be added. An upgrade first tries an atomic conversion under the configured priority policy. If it must wait, it yields the entire transaction's held resources and requeues the full scope with the requested resources in WRITE mode and all other resources in their original modes. Its own old read rights cannot block the new request. A downgrade queues the selected resources in READ mode while keeping other resources held, and may wait behind an earlier writer. No-op conversions return immediately.

upgradeWrite returns a frozen { smooth, intervenedKeys }. Per-resource WRITE-acquisition counters identify exactly which logical keys another request acquired in WRITE mode after this transaction yielded and before it reacquired them. Merely yielding, waiting or admitting another reader does not count; the transaction's own reacquisition is excluded. smooth is true exactly when that list is empty, even if the upgrade waited. The list may include resources outside the explicitly upgraded subset because the fallback yields the whole scope. Re-read affected data after an intervening writer. These counters track cooperative lock acquisitions, not writes made outside Aragami.

Await each conversion before accessing the resources. Conversions on one manager are serialized; callback completion cancels unfinished conversions and releases its resources. Redis continues renewing the transaction and its queued conversion while waiting. Reentry remains unsupported.

Redis read/write locks retain lease renewal, notifications, polling fallback and acquisition-timeout errors. A manager conversion waits until granted or until its callback/driver ends.

Queues: work for a consumer

A queue separates creating work from executing it. An application can enqueue a mail request immediately, then let a worker handle it later. Consumers on the same queue compete for jobs, and each claim enters a processing state until acknowledged or recovered.

Queues also follow the two key layers. In this example, jobs is the model namespace and mail is the queue within it:

@CachePrefix('jobs')
class Job { userId!: string; }

await aragami.queueAdd(Job, { userId: 'u1' }, { key: 'mail' });
await aragami.runQueueOnce(Job, (job) => sendMail(job.userId), 'mail');

queueAdd accepts an instance or a class plus an object and returns the actual enqueued instance. Ready jobs normally follow FIFO order; prior: true inserts an immediate job at the front.

runQueueOnce waits for one executable job, restores the model and invokes your callback. Success automatically acknowledges the claim. Failure requeues that claim at the front and rethrows the error; if both processing and recovery fail, AggregateError reports both errors and leaves the claim recoverable.

For manual claiming, queueGather returns an executable job or undefined, while queueGatherBlocking waits for one. Both enter processing state. queueResumeAll explicitly restores processing jobs, preserving claim order and appending by default; its prior argument restores them to the front. Recovery is explicit, with no automatic processing timeout. Distinct task IDs and fresh claim tokens prevent an old receipt from acknowledging a later claim.

queueLength, queueItems and isQueueEmpty describe pending work, including future delayed jobs and excluding processing claims. Items show ready jobs first, followed by delayed jobs in due-time and insertion order. queueClear removes pending work and preserves processing claims for recovery.

Delayed jobs

A delayed job is ordinary queued work with an earliest execution time. This is useful for sending a reminder later or scheduling an action at a known time. It stays in the same queue, so existing consumers can handle immediate and delayed jobs together.

Choose a relative delay in milliseconds or an absolute runAt time:

await aragami.queueAdd(Job, { userId: 'u1' }, { key: 'mail', delay: 5_000 });
await aragami.queueAdd(Job, { userId: 'u2' }, {
  key: 'mail', runAt: new Date(Date.now() + 60_000),
});

delay must be nonnegative and finite. runAt accepts a valid Date or finite epoch-millisecond timestamp; the two options cannot be combined. Past times and zero delays are immediately executable. Consumers and queries promote due jobs, so no separate scheduler service is required. prior takes effect when a delayed job becomes ready, preserving stable order within each due batch.

Redis uses server time for relative delays and promotion. Absolute times require application and Redis clocks to agree. Retry and recovery do not apply the original delay again.

Continuous workers

queueSubscribe runs the consume loop for you: wait for work, process it, acknowledge it, and continue. Each worker runs one callback at a time. Create several handles on the same queue when you want competing consumers to process jobs in parallel.

The returned handle controls the worker's lifetime:

const worker = await aragami.queueSubscribe(Job, 'mail', (job) => sendMail(job.userId), {
  retryDelay: 1_000,
  onError: reportError,
});

Successful processing continues immediately. A failed claim or callback is reported through onError, or logged when no handler is supplied, then retried after retryDelay (default 200 ms). Failed jobs return to the front. Error-handler failures are caught. The interval must be nonnegative and finite; 0 adds no retry wait.

Call await worker.unsubscribe() to stop consumption; it is idempotent and interrupts idle or retry waits. An already running callback may finish and be acknowledged; await worker.done lets the caller wait for that drain. A callback can unsubscribe its own worker, but should not await its own done. Omitting the queue key consumes default.

Pub/Sub: messages for every subscriber

Pub/Sub broadcasts an event to every active subscription on its topic. It suits notifications such as a user update that several parts of an application need to observe. Each subscriber gets its own restored model instance, and a subscription can listen to several keys within one class namespace.

For example, the User model can be broadcast on the updated and removed topics:

const sub = await aragami.subscribe(User, ['updated', 'removed'], (message, key) => {
  refreshUserView(message.id, key);
});
await aragami.publish(User, { id: 'u1', name: 'Alice' }, { key: 'updated' });
await sub.unsubscribe();

Queue consumers share work; Pub/Sub subscriptions each receive matching messages. Broadcasting is live and at most once, with no history or offline replay, following Redis Pub/Sub delivery semantics. Redis database numbers do not separate channels, so use different Pub/Sub prefixes for different environments.

publish accepts either publishing form used by queueAdd, returns the published instance, and waits for publication rather than subscriber callbacks. subscribe passes the restored message and logical key to its handler. An omitted subscription key listens to default.

Handlers run concurrently by default. serial: true processes messages in order within that subscription while other subscriptions remain independent. Handler failures go to onError, or are logged without the message body; error-handler failures are caught and later messages continue without retry.

Unsubscribing one handle preserves other subscriptions, stops future delivery and discards queued serial callbacks. Running callbacks can finish. The Promise-based Redis subscription returns after channel acknowledgment, using a shared connection with channel reference counts. Public topics are isolated from internal utility notifications.

Publishing from an RxJS stream

publishRx connects an existing Observable to Pub/Sub. It is useful when updates already flow through RxJS and you want publication results, errors and completion to remain in that pipeline.

Given an updates$ stream of user values, publication can become another Observable:

const published$ = aragami.publishRx(User, updates$, { key: 'updated' });
const publishing = published$.subscribe({
  next: recordPublication, error: reportError, complete: publicationFinished,
});

This is a synchronous method returning a cold Observable: it subscribes to updates$ only when subscribed. Values publish sequentially, with each successful publication emitting the actual model instance. Source completion waits for queued publications; source or publication errors reach the output's error channel.

Call publishing.unsubscribe() when stopping. This detaches the source and discards unstarted publications. An already started publication may finish, but cannot emit to the cancelled subscription. Subscribing again runs the source and publications again.

Receiving messages as an RxJS stream

subscribeRx exposes incoming Pub/Sub messages as a cold Observable. It fits applications that want to filter, combine or react to events with RxJS operators instead of managing a callback subscription handle.

Subscribe by model and one or more topic keys:

const receiving = aragami.subscribeRx(User, ['updated', 'removed']).subscribe({
  next: (message) => refreshUserView(message.id),
  error: reportError,
});

Each Rx subscription owns an independent live subscription and emits restored messages. Call receiving.unsubscribe() to stop it. Omitted keys use default; serial and callback-handler options are not part of this API. Setup and decoding failures error the stream. Cancellation during setup also cleans up the eventual handle.

Redis setup is asynchronous, so the Rx subscribe call is not a channel-ready acknowledgment, and the stream does not inject a readiness message. Use the Promise-based subscribe when a publisher needs to wait explicitly for its subscriber to be ready.

Election and leader selection

Election lets a group of active participants agree which members should act as coordinators. For example, several application instances may be able to run a periodic task, while only the current leader should schedule it. Aragami exposes the membership view so the application can make that decision.

elect is a synchronous method returning a cold Observable<ElectionState>. Every subscription joins as a separate participant; calling the method alone joins nothing. The first snapshot means listening and registration have finished, and later snapshots describe membership changes until unsubscribe or destruction.

Each snapshot is a class instance with a fixed selfId, sorted onlineIds, and an isLeader method:

const membership = aragami.elect('workers').subscribe((state) => {
  console.log(state.selfId, state.onlineIds);
  updateLeadership(state.isLeader());
});

The smallest online ID is the leader by default. state.isLeader(2) selects up to two leaders by checking whether this participant is among the two smallest online IDs. Leaving members do not renumber survivors. Counts must be nonnegative safe integers; zero selects no leaders. Call membership.unsubscribe() to withdraw this participant.

IsLeader is the corresponding pipe operator, while isLeader belongs to the snapshot. It maps membership to a boolean; use distinctUntilChanged if only leadership transitions matter:

import { IsLeader } from 'aragami';
import { distinctUntilChanged } from 'rxjs';

const leadership = aragami.elect('workers').pipe(IsLeader(2), distinctUntilChanged())
  .subscribe({ next: updateLeadership, error: reportError });

elect(user) uses the model's namespace and LockKey; elect() uses the same group as elect('default'). An object's multiple resource keys form one deduplicated, sorted group identity. Asynchronous metadata resolves only after subscription.

Redis shares membership across processes using leases, heartbeat renewal, notifications and polling fallback. A lost or expired lease errors the stream; a new subscription gets a new ID. Registrations have private tokens to protect against stale cleanup, and per-group sequence keys persist to prevent ID reuse. Leader snapshots describe membership and do not fence external writes.

Memory elections run within one driver instance. Facades injecting that same driver share membership; separate memory drivers have separate elections. Calling leadership.unsubscribe() also withdraws its participant. Unsubscribe immediately stops local delivery and timers, while Redis removal finishes asynchronously.

Function wrappers and method decorators

Wrappers apply cache or lock behavior to functions that already express your business operation. useCache, useLock and useRWLock take a key selector alongside the function, letting callers reuse the wrapped operation without repeating resource selection.

For service methods, WrapDecoratorBuilder supplies UseCache, UseLock and UseRWLock. Parameter decorators identify which arguments provide keys: WithKey for caching and WithLockKey for both lock types.

A read method can therefore declare its resource at the method boundary:

import { WithLockKey, WrapDecoratorBuilder } from 'aragami';

const { UseRWLock } = new WrapDecoratorBuilder(() => aragami).build();
class UserService {
  @UseRWLock(RWLockMode.READ)
  async read(@WithLockKey() user: User) { return readUser(user); }
}

Decorators preserve this, return values and exceptions. Instance factories and parameter key factories may be asynchronous; lock parameters may select several resources.

Configuration

Configuration controls how long cached data or leases live, how lock waiters retry, and how physical resource namespaces are separated. Common lock, rwLock and pubSub settings apply to both implementations; redis.lock, redis.rwLock and redis.pubSub override them in Redis mode.

The main namespace settings are:

| Setting | Default | Purpose | | --- | --- | --- | | defaultTTL | 0 | Cache lifetime in milliseconds; zero means no expiry | | redis.lock.prefix | _lock | Ordinary lock namespace | | redis.rwLock.prefix | _rwlock | Independent read/write lock namespace | | pubSub.prefix | _pubsub | Broadcast namespace; overridable by redis.pubSub.prefix | | redis.election.prefix | _election | Election namespace | | redis.queueKey, redis.queueBackupKey | _queue, _queue_backup | Pending and processing queue storage prefixes |

For redis.rwLock, duration defaults to 5000 ms, acquireTimeout to 0 (wait indefinitely), retryDelay to 200 ms, retryJitter to 100 ms, and extensionThreshold to 500 ms, capped at half the lease. stacked defaults to false and coordinates individual acquisition attempts. The strategy defaults to WRITE_FIRST and multiKeyBypass to false. Ordinary Redis locks have their own timing configuration and retain the legacy retryCount and automaticExtensionThreshold options.

For redis.election, duration defaults to 5000 ms, heartbeatInterval to duration / 3, and pollInterval to 1000 ms. Intervals must be positive and finite, with the heartbeat interval shorter than the lease. Shared election data is retained for the longest active lease when participants use different durations.

Physical isolation markers keep read/write locks, broadcasting and election separate from ordinary locks even when prefixes match. The queue layout uses __aragami_queue_v2__ with default or custom prefixes; old ready/backup lists are not read, migrated or deleted.

Memory, Redis and custom drivers

Redis is the shared backend for applications running across processes. Memory implements the same APIs with local state, and is especially useful for tests or applications that need coordination within a single process.

The sharing boundaries are deliberate:

| Capability | Memory scope | Redis scope | | --- | --- | --- | | Cache, locks and queues | One driver instance | Shared Redis storage and configured namespaces | | Election | One driver instance | Shared election prefix and resource group | | Pub/Sub | Same process, prefix and both key layers | Shared server channels and prefix |

useDriver injects a BaseDriver subclass and takes precedence over Redis configuration. BaseDriver, MemoryDriver and RedisDriver are exported for direct use. Existing Buffer-based queue drivers remain supported through compatibility adapters; drivers can also implement the receipt-based claim, acknowledgment and recovery methods. Built-in Buffer adapters require the exact Buffer returned by gathering a job.

Call await aragami.destroy() when the instance is no longer needed. Destruction stops its workers, completes active Rx streams, drains asynchronous subscription cleanup and closes driver resources. Blocking queue operations finish with undefined without calling job callbacks. Redis retains unacknowledged work for explicit recovery after restart; memory state is cleared.

Development and verification

The test suite exercises both backends, including concurrency, class restoration, delayed work, cancellation and resource cleanup. Choose the complete run or an individual backend as needed:

| Command | What it checks | | --- | --- | | npm test | Complete memory and Redis suites | | npm run test:memory | Memory only; no Docker required | | npm run test:redis | Redis with a Compose-managed test service | | ARAGAMI_TEST_REDIS_IMAGE=redis:5.0.0-alpine npm run test:redis | Minimum supported Redis version | | npm test -- --coverage | Coverage for source and the package entry | | npx tsc --noEmit | Type checking | | npm run build | CommonJS, native ESM and declarations | | npm run test:package | Built CJS/native ESM package smoke checks |

Run npm ci first. Project checking and declarations use native TypeScript 7.0.2; the TypeScript 6 compatibility API supports TypeScriptToLua, ts-jest and lint tooling. Builds regenerate the built-in strategy Lua through the same compiler used by custom instances. The Redis project's TypeScript harness manages only docker compose: Jest runs on the host, each run gets a unique project and ephemeral loopback port, and startup failures also trigger cleanup. Start your Docker runtime beforehand; the harness has no macOS or Colima dependency. This development machine uses Colima and stops it after verification. Setting ARAGAMI_TEST_REDIS_URI for tests/package-smoke.mjs adds Redis checks to both built formats.

License

Aragami is released under the MIT license.