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

@jplevyak/actjs

v0.3.1

Published

A self-hosted actor framework for TypeScript: SWM + event-sourced actors, durable snapshots, JSON-RPC over WebSocket, codegen-typed client SDK.

Readme

actjs

A self-hosted actor framework for JavaScript and TypeScript backends.

Actors are addressable, single-writer objects with durable state. Each actor's mailbox runs serially; concurrent callers get JSON-RPC over a WebSocket and see state changes as RFC 6902 patches (single-writer mailbox actors) or raw events (event-sourced actors). Class source is a first-class entity with semver, content-addressed bytes, and a frozen dependency graph — once a client has resolved a manifest, every actor call rooted in that request runs against the exact same class versions end-to-end.

  • Concurrency model. Single-writer mailbox per actor; opt-in event sourcing per class.
  • Wire protocol. URL-versioned REST + JSON-RPC 2.0 over WebSocket; SSE fallback.
  • Frontend story. Typed @jplevyak/actjs/client plus @jplevyak/actjs/bindings/react and @jplevyak/actjs/bindings/svelte bindings; actctl codegen emits a .d.ts bundle and a manifest.json per build so calling a non-existent method is a compile error.
  • Storage. Valkey (hot path, mailboxes, locks) + Postgres (system of record: classes, snapshots, event log, audit).

See PLAN.md for the v1 design, tasks/ROADMAP.md for what's queued post-8.2, DESIGN.md for the architecture, MONOREPO.md for the frontend↔backend monorepo layout, and CHANGELOG.md for what shipped in which release.


Concepts

  • Actor. Addressable by (class, id). Methods marked @handler are exposed over the wire. The actor's mailbox is the single serial writer — handlers never race with each other.
  • Class. Published TypeScript source, versioned by semver, content-addressed by sha256(source). Publishing is immutable; deprecation is a separate lifecycle.
  • Manifest. A frozen ClassName → Version map resolved from a set of root requirements. Once a client pins a manifest sha, every call rooted at that request uses the same versions, including through actor-to-actor calls.
  • SWM actors. Subclass Actor<S>. Handlers mutate this.state; the host snapshots on a trailing debounce. Subscribers see RFC 6902 patches.
  • ES actors. Subclass EventSourced<S, E>. Handlers return events; the host appends atomically, then folds with the user's pure reduce. Subscribers see the appended events. Replay is the source of truth.
  • Reminders. actjs.scheduleAt(when, type, payload) enqueues a durable tell that fires on a wall-clock deadline; survives restarts.
  • Capabilities. Short-lived signed tokens scoped to an actor and a method set. Useful for shareable read links and SSR.

Quick start

docker compose up --build           # actjs + Valkey + Postgres

Publish a class:

actctl publish ./src/actors/Cart.ts --version 1.4.2

Resolve a manifest and grab its sha:

actctl manifest show --root [email protected]
# → { sha256: "...", resolved: { Cart: "1.4.2", Item: "1.0.9" } }

Generate the typed client bundle:

actctl codegen --out ./client-types

Use it from a React app:

import { Client } from '@jplevyak/actjs/client';
import { useActor, useActorValue } from '@jplevyak/actjs/bindings/react';
import type { Cart } from './client-types';

const client = new Client({ url: '/api', token });

function CartView({ id }: { id: string }) {
  const cart = useActor<Cart>('Cart', id);
  const total = useActorValue<Cart, number>('Cart', id, (c) => c.total);
  return (
    <>
      <p>Total: {total}</p>
      <button onClick={() => cart.call.addItem({ sku: 'X', qty: 1 })}>Add</button>
    </>
  );
}

Writing actor classes

SWM (single-writer mailbox)

import { Actor, handler } from '@jplevyak/actjs';

interface CartState {
  items: Array<{ sku: string; qty: number }>;
}

export class Cart extends Actor<CartState> {
  override onInit(): void {
    this.state = { items: [] };
  }

  @handler('addItem')
  addItem(args: { sku: string; qty: number }): number {
    this.state.items.push(args);
    return this.state.items.length;
  }

  @handler('total')
  total(): number {
    return this.state.items.reduce((s, i) => s + i.qty, 0);
  }
}

Inside a handler you have access to a per-instance this.actjs bridge:

| Method | What it does | | -------------------------------------- | ---------------------------------------------------------------- | | this.actjs.self | { class, id } of the current actor | | this.actjs.call(ref, method, args) | Request/response to another actor (runs under the same manifest) | | this.actjs.tell(ref, type, payload) | Fire-and-forget; entry is durable before the call returns | | this.actjs.scheduleAt(when, type, p) | Durable reminder; auto-rebound from PG on restart | | this.actjs.now() | Test-seam clock | | this.actjs.log | Structured pino logger; requestId / actorId already bound | | this.actjs.abort(reason) | Aborts the in-flight handler; the caller sees ActorAbort |

ES (event-sourced)

import { EventSourced, handler } from '@jplevyak/actjs';

interface LedgerState {
  balance: number;
}
type LedgerEvent = { type: 'Deposited'; amount: number } | { type: 'Withdrawn'; amount: number };

export class Ledger extends EventSourced<LedgerState, LedgerEvent> {
  initialState(): LedgerState {
    return { balance: 0 };
  }
  reduce(s: LedgerState, e: LedgerEvent): LedgerState {
    return e.type === 'Deposited'
      ? { balance: s.balance + e.amount }
      : { balance: s.balance - e.amount };
  }
  @handler('deposit')
  deposit(args: { amount: number }): LedgerEvent[] {
    return [{ type: 'Deposited', amount: args.amount }];
  }
}

ES handlers don't write state directly — they return events. The host appends them atomically, then folds via reduce. Cold-start reads the last snapshot + every event after it and rebuilds state by re-applying reduce.

Authorization

Each class can declare a policy() static. The runtime invokes it on every call before dispatch:

static policy(p: Principal, action: PolicyCtx<Cart>): PolicyDecision {
  if (action.kind === 'call' && action.method === 'addItem') {
    return p.sub === action.actor.state.ownerId ? 'allow' : 'deny';
  }
  return 'deny';
}

A YAML DSL for the common owner-only / role-match / tag-match cases is roadmapped — see tasks/ROADMAP.md Tier 5. JS policy() statics are the supported v1 surface.

Migrations

When a class is registered at a newer version than a previously persisted snapshot, the host runs migrate() (SWM) or migrateEvent() (ES) before opening the actor to traffic. The prior snapshot is retained at seq = -1 for the configured rollback window.

override migrate(prev: unknown, prevVersion: string): CartV2State {
  const old = prev as CartV1State;
  return { ...old, currency: 'USD' };
}

Versioning, manifests, and pinning

# Publish a new version of Cart.
actctl publish ./src/actors/Cart.ts --version 1.4.2

# Resolve a dep tree.
actctl manifest show --root [email protected] --range Item@^1.0.0
# → sha256: ..., resolved: { Cart: "1.4.2", Item: "1.0.9" }

# Deprecate (hides from new resolutions; pinned clients keep working).
actctl deprecate [email protected] --reason "use 1.4.2+" --grace 30d

# Inspect who's still pinned to a deprecated version.
actctl manifest in-use

Manifests are content-addressed: the same set of resolved versions always hashes to the same sha. The SDK build embeds the sha returned by actctl codegen and sends it as X-Actjs-Manifest: <sha> on every call. Server behavior:

| Pin status | Server response | | ----------------------------------- | ----------------------------------------------------- | | Unknown sha | 400 ManifestUnknown | | Sha references a deprecated version | 200 with Warning: 299 - "VersionDeprecated <ref>" | | Sha references an expired version | 410 Gone with the offending refs | | Healthy | 200, no warning |

The resolver rejects with a structured 409 DepConflict that includes the path through deps producing every incompatible constraint, so the caller can see exactly which transitive range disagreed.

Per-actor activation policy:

  • Sticky by default — an actor keeps running the class version it was created with. Older versions are loaded by the in-process ClassLoader (LRU + refcount).
  • Floating opt-inregister({ floating: true }) makes every activation use the latest registered version, walking migrate() if the snapshot predates it.
  • Refusing to run older code against newer state is a hard error (ManifestRegression).

HTTP API

The HTTP layer is Fastify + Zod; routes are validated and typed end-to-end. OpenAPI 3.1 is exported at GET /openapi.json.

REST

# Create an actor (mints a fresh id; onInit fires on first call).
curl -X POST http://127.0.0.1:3000/v1/actors/Cart -d '{}' \
  -H "Content-Type: application/json"
# → 201 { class: "Cart", id: "0190..." }

# Invoke a handler. Idempotency-Key turns network retries into a
# one-line contract; replays are byte-identical and carry
# `Idempotency-Replayed: true`.
curl -X POST http://127.0.0.1:3000/v1/actors/Cart/<id>/addItem \
  -H "Content-Type: application/json" \
  -H "X-Actjs-Manifest: <sha>" \
  -H "Idempotency-Key: <uuid>" \
  -d '{"sku": "X", "qty": 1}'

# Read the snapshot.
curl http://127.0.0.1:3000/v1/actors/Cart/<id>

# Tombstone.
curl -X DELETE http://127.0.0.1:3000/v1/actors/Cart/<id>

Errors use application/problem+json with a stable code field: DepConflict, ManifestUnknown, Gone, Forbidden, MailboxFull, SchemaInvalid, SyntaxInvalid, ManifestRegression, etc. The SDK switches on code, not on HTTP status or title.

WebSocket / JSON-RPC

A single endpoint at /v1/ws speaks JSON-RPC 2.0 and multiplexes calls and subscriptions over one connection. Methods:

  • actor.call(class, id, method, args){result}.
  • actor.subscribe(class, id){subscriptionId}. The first actor.event notification carries the initial snapshot; subsequent notifications carry patch (SWM) or event (ES).
  • actor.unsubscribe(subscriptionId){ok}.
const ws = new WebSocket('ws://127.0.0.1:3000/v1/ws');
ws.onmessage = (m) => console.log(JSON.parse(m.data));
ws.send(
  JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'actor.subscribe',
    params: { class: 'Cart', id: '<actor-id>' },
  }),
);
// ← { id: 1, result: { subscriptionId: '...' } }
// ← { method: 'actor.event', params: { kind: 'snapshot', data: { ... } } }
// ← { method: 'actor.event', params: { kind: 'patch', patch: [...] } }

Heartbeats: server pings every 30s; the connection closes with 1001 heartbeat timeout if no pong arrives within 90s. Per-actor subscriber cap (default 1000) protects fanout from a single hot key.

SSE fallback

Some corporate proxies eat WS traffic. GET /v1/sse/:class/:id returns a server-sent-event stream of the same actor.event notifications; actor.call is still a regular POST. The SDK falls back automatically.

Auth

The server takes a BYO auth(req): Promise<Principal> hook at boot. The resolved principal is bound on req.principal and threaded into the class policy() static.

import { buildApp } from '@jplevyak/actjs/server';

const app = await buildApp({
  driver,
  runtime,
  auth: async (req) => verifyJwt(req.headers.authorization),
});

Token formats — JWT, opaque session, capability — are entirely operator-chosen. The runtime only sees the resulting Principal.


Frontend SDK

@jplevyak/actjs/client

import { Client } from '@jplevyak/actjs/client';
import type { Cart } from './client-types';

const client = new Client({ url: '/api', token });
const cart = client.actor<Cart>('Cart', cartId);

await cart.call.addItem({ sku: 'X', qty: 2 });
const total = await cart.get.total();

cart.subscribe((state) => render(state)); // WS under the hood
  • Method signatures come from the actctl codegen .d.ts bundle.
  • One WebSocket per Client, multiplexed across every subscribe and call. Exponential backoff + replay on reconnect.
  • Optimistic updates (SWM): cart.optimistic(draft => draft.items.push(x)) applies locally via Immer, sends the call, reverts on failure.
  • Offline queue: mutations persist to IndexedDB keyed by their Idempotency-Key and replay on reconnect.
  • ES clients receive events and fold them with the generated reducer client-side; the local state is byte-identical to the server's by construction.

@jplevyak/actjs/bindings/react

const cart = useActor<Cart>('Cart', id); // Suspense for initial load
const total = useActorValue<Cart, number>('Cart', id, (c) => c.total);

Built on useSyncExternalStore for updates, Suspense for cold load, useTransition for optimistic. @jplevyak/actjs/bindings/react/server exports fetchActor(class, id, { manifest }) for RSC hydration.

@jplevyak/actjs/bindings/svelte

<script lang="ts">
  import { actor } from '@jplevyak/actjs/bindings/svelte';
  import type { Cart } from './client-types';
  const cart = actor<Cart>('Cart', id);
</script>

<p>Total: {$cart.total}</p>

Svelte 5 runes API; $cart is a readable store with call, loading, error siblings.


actctl CLI

The subcommands marked shipped are wired today. Everything else is roadmapped to 8.2b — see tasks/ROADMAP.md Tier 3.

| Command | Status | What it does | | ------------------------------------------ | ------- | ---------------------------------------------------------- | | actctl codegen | shipped | Generate the .d.ts bundle + manifest.json for the SDK | | actctl publish --name --version --source | shipped | Publish a class version (Ed25519 --sign optional) | | actctl key add --kid --pem | shipped | Register a publish signing key with the server | | actctl key revoke --kid | shipped | Revoke a signing key | | actctl dev | 8.2b | Watch local class dir; republish pre-release versions | | actctl list | 8.2b | List class versions and their deprecation state | | actctl deprecate <ref> --grace 30d | 8.2b | Mark a version deprecated with a grace window | | actctl promote <ref> --env prod | 8.2b | Promote a version through environments | | actctl manifest show --root <ref> | 8.2b | Resolve a manifest; show the pinned versions and sha | | actctl manifest in-use | 8.2b | Which manifest shas are currently being sent by clients | | actctl shell | 8.2b | Admin REPL; arbitrary await snippets routed through call | | actctl actor inspect <id> | 8.2b | State, recent envelopes, mailbox depth, resolved manifest | | actctl migrate dry-run | 8.2b | Replay a snapshot corpus through every migration chain | | actctl logs follow --actor <id> | 8.2b | Tail structured logs for one actor | | actctl audit follow | 8.2b | Tail the audit log (filterable by principal / action) |


Server runtime (embedding)

For tests and embedded deployments, talk to the Runtime directly:

import { Runtime } from '@jplevyak/actjs/runtime';
import { MemoryStorageDriver } from '@jplevyak/actjs/storage';
import { asClassName, asVersion, mkActorId } from '@jplevyak/actjs/types';

const driver = new MemoryStorageDriver();
await driver.init();
const runtime = new Runtime(driver);
runtime.register({
  name: asClassName('Cart'),
  version: asVersion('1.0.0'),
  ctor: Cart,
});

const id = mkActorId();
await runtime.call(asClassName('Cart'), id, 'addItem', { sku: 'X', qty: 1 });

await runtime.shutdown();
await driver.close();
  • MemoryStorageDriver is the in-process driver used by tests and actctl dev.
  • ValkeyPgStorageDriver is the production driver (Valkey for hot state, PG for the durable record).

@jplevyak/actjs/test (actjs/test export) wraps this with a TestRuntime that mints actors via t.actor(Ctor), drives time deterministically via t.advanceTime(ms) (fires due reminders), and ships framework-agnostic assertions:

import { TestRuntime, assertSnapshot, assertEmitted } from '@jplevyak/actjs/test';

const t = await TestRuntime.create({ classes: { Cart } });
const cart = t.actor(Cart);
await cart.call.addItem({ sku: 'X', qty: 1 });
await assertSnapshot(cart, { items: [{ sku: 'X', qty: 1 }] });
await t.close();

replayMigrations({ ctor, snapshots }) reports a per-snapshot diff through the migration chain. Property-test integration (fast-check arbitraries) is roadmapped to 8.2c. See docs/testing.md.


Observability

Shipped today:

  • Logs. pino via the Logger surface in actjs/log; one JSON line per event with requestId, actorId, class@version, principal.sub, manifestSha. Sensitive headers (Authorization, Idempotency-Key, capability JWT) are redacted. Per-subsystem env overrides (ACTJS_LOG_LEVEL_DRIVER, etc.); the driver defaults to warn.
  • Metrics. prom-client registry exposed at /metrics when the Runtime is built with a MetricsRegistry:
    • actjs_actor_message_total{class,method,outcome} — method label allow-listed (default 50/class → _other).
    • actjs_actor_active{class,version}, actjs_actor_mailbox_depth{class}
    • actjs_clients_by_manifest{sha} (capped via the manifest tracker)
    • actjs_manifest_resolution_seconds
    • actjs_event_append_total{class}, actjs_event_snapshot_total{class}
    • actjs_policy_decision_total{class,decision}, actjs_rate_limit_drop_total{subject}, actjs_capacity_exhausted_total{class}, actjs_capability_minted_total{class}
    • Standard Node + process collectors See docs/observability.md for the full runbook and starter PromQL.
  • Audit. Every publish, deprecate, signing-key change, capability mint, admin RPC, and tombstone is written to the audit PG table via the strict-mode Auditor (best-effort opt-in).

Roadmapped (8.1b — see tasks/ROADMAP.md):

  • OpenTelemetry SDK + W3C trace-context propagation through Envelope.causation.
  • ops/grafana/dashboards/ JSON bundle, ops/prometheus/alerts.yml, compose.observability.yml.
  • S3 audit mirror (optional, append-only with object-lock).

Requirements

  • Node.js ≥ 20
  • Valkey (or Redis) on REDIS_URL
  • Postgres on DATABASE_URL (or POSTGRES_URL)

| Var | Default | Purpose | | -------------- | ------- | -------------------------- | | PORT | 3000 | HTTP listen port | | REDIS_URL | — | Valkey hot-path connection | | DATABASE_URL | — | Postgres connection |

Without DATABASE_URL the server falls back to the in-memory storage driver with a startup warning. Suitable for tests and demos; not for production.


Scripts

| Script | What it does | | ----------------------- | ---------------------------------------------- | | npm run build | tsc -bdist/ | | npm run dev | tsx watch src/main.ts for hot reload | | npm start | node dist/main.js (requires a prior build) | | npm run typecheck | tsc --noEmit | | npm run lint | ESLint over src/, tests/, configs | | npm run format | Prettier write | | npm test | Vitest run | | npm run test:coverage | Vitest + v8 coverage with thresholds |


Files

| Path | Purpose | | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | | src/actor.ts | Actor<S> base class (SWM) | | src/event-sourced.ts | EventSourced<S, E> base class | | src/replica.ts | Replica<S> opt-out-of-persistence base class | | src/handler.ts | @handler decorator + getHandlers | | src/types/ | ActorId, ClassName, Version, Manifest, Envelope<T>, Principal | | src/storage/ | StorageDriver interface + memory and Valkey/PG drivers (incl. fence token) | | src/runtime/ | Runtime, ActorHost, Mailbox, ClassLoader, reminder dispatcher | | src/registry/ | Resolver, publisher, manifest tracker, MemorySigningKeyRegistry | | src/policy/ | policy() evaluation, capability JWTs, MemoryBlocklist | | src/limits/ | RateLimiter (token bucket), CapacityExhaustedError | | src/audit/ | Auditor, AUDIT_ACTIONS, strict/best-effort write modes | | src/log/ | Logger surface (pino-backed, noop, collecting), redaction list | | src/metrics/ | MetricsRegistry (prom-client) + cardinality guards | | src/wire/ | JSON-RPC envelope types shared between server + client | | src/codegen/ | actctl codegen.d.ts + manifest emitter | | src/client/ | @jplevyak/actjs/client — WS transport, RPC, subscriptions, offline queue | | src/bindings/ | @jplevyak/actjs/bindings/react + @jplevyak/actjs/bindings/svelte adapters; refcounted ActorStore | | src/cli/ | actctl CLI | | src/test/ | @jplevyak/actjs/testTestRuntime, assertions, replayMigrations | | src/server/ | Fastify app, REST routes, WS endpoint, hooks (pin, idempotency, auth) | | src/server/subscription-registry.ts | Per-actor subscription fan-out for WS / SSE | | migrations/ | Postgres schema (0001_init, 0002_reminders, 0003_signing_keys, 0004_actor_fence) | | docs/ | Operator runbooks (auth, observability, ops-hardening, testing) | | tasks/ | Per-phase implementation tasks + ADRs + ROADMAP.md | | tests/ | Vitest unit + integration tests | | Dockerfile | Multi-stage build, distroless final stage | | docker-compose.yml | Local stack: actjs + Valkey + Postgres |


Project status

actjs is built phase-by-phase against PLAN.md. Phases 0 through 8.2 have shipped, plus the Phase 9 cluster-seam audit; follow-up work that was deferred from each phase is tracked in tasks/ROADMAP.md. The CHANGELOG is the authoritative record of what has shipped in each release; this README describes the v1.0 surface area the PLAN targets, with explicit roadmap markers wherever a piece is queued for a follow-up. Per-phase task files and ADRs live in tasks/.

Phase 9 (multi-node clustering) is sketched in PLAN.md but deliberately not in v1. The 2026-05-20 cluster-seam audit verified the directory boundary, idempotency, durable inbox, manifest envelope, reminder durability, and storage driver boundary; the audit closed three gaps (fence-token plumbing through driver + host, WS manifest-pin capture per connection, reminders dispatcher key parameterization) so the v2 implementation tasks (9.1 through 9.7) land without rewriting earlier phases. See phase-9-cluster-seams.adr.md.