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

@sigx/actors-k8s

v0.2.0

Published

Kubernetes membership provider for @sigx/actors — host liveness via coordination.k8s.io Leases, no extra store

Readme

@sigx/actors-k8s

Kubernetes membership provider for @sigx/actors clustering. Host liveness and peer discovery ride coordination.k8s.io Leases — the same primitive kubelet uses for node heartbeats — so a cluster running in Kubernetes needs no extra store for membership. No Kubernetes client library either: the API surface this package touches is six verbs on one resource, spoken in plain HTTPS + JSON.

Membership and the actor directory are independent seams; the directory stays store-backed. Compose them:

import { createHost } from '@sigx/actors/host';
import { clusterPlacement } from '@sigx/actors/cluster';
import { k8sMembership } from '@sigx/actors-k8s';
import { redisDirectory } from '@sigx/actors-redis';

const host = createHost({
    actors,
    storage,
    placement: clusterPlacement({
        membership: k8sMembership(),
        directory: redisDirectory(redis),
        advertise: `http://${process.env.POD_IP}:7311`,
        secret: process.env.HOST_SECRET
    })
});

Inside a pod everything is discovered: API server from the service env vars, namespace / token / CA from the ServiceAccount mount, with the bound token re-read on rotation. Call k8sMembership() once per host.

Options

| Option | Default | Meaning | |---|---|---| | namespace | ServiceAccount namespace, else default | where the Leases live | | clusterName | default | value of the sigx.dev/cluster label — two clusters can share a namespace | | labels | — | extra labels stamped on the own Lease AND selecting peers | | leasePrefix | sigx | Lease names are {leasePrefix}-{hostId} | | heartbeatMs | 5000 | Lease renewal cadence | | ttlMs | 15000 | liveness TTL, serialized as spec.leaseDurationSeconds (ceiled to seconds) — missed renewals past this = dead | | clockSkewMs | 2000 | slack added to peer freshness checks | | relistMs | 60000 | reconciling LIST cadence under the watch; 0 disables | | apiServer | in-cluster env, else https://kubernetes.default.svc | API server origin | | token | ServiceAccount token file | bearer token, or a provider function | | ca | ServiceAccount ca.crt | PEM bundle for the API server | | fetch | node:https shim | transport override (tests, kubectl proxy dev) | | watchBackoff | { minMs: 250, maxMs: 5000 } | watch reconnect backoff bounds |

How it works

Each host owns one Lease; nobody else ever writes it:

{
    "apiVersion": "coordination.k8s.io/v1",
    "kind": "Lease",
    "metadata": {
        "name": "sigx-s.k3f9a2",
        "labels": { "sigx.dev/cluster": "default" },
        "annotations": { "sigx.dev/descriptor": "{\"hostId\":\"s.k3f9a2\",…}" }
    },
    "spec": {
        "holderIdentity": "s.k3f9a2",
        "leaseDurationSeconds": 15,
        "renewTime": "2026-07-29T12:04:05.123456Z"
    }
}
  • Heartbeat — a merge-patch of spec.renewTime every heartbeatMs; no read-modify-write, no resourceVersion races.
  • Discovery — every host runs the standard list-then-watch loop over Leases matching its labels; a peer is live while its renewTime is within its own declared duration. Watch events for renewals never bump the membership view's version — only descriptor-set changes (join, leave, drain, expiry) do, so heartbeats cause zero onChange traffic.
  • Self-fencing — renewal failures past ttlMs fire onSelfSuspect once, exactly like the Redis provider: the host stops claiming actors and deactivates what it holds. A broken watch never fences — that is staleness, covered by the relistMs safety net.
  • Graceful exitsetStatus('leaving') patches the descriptor immediately (drain is visible now, not next beat); leave() deletes the Lease. A crashed host simply stops renewing and ages out.
  • Versionsview().version is a local monotonic counter. Kubernetes resourceVersion is an opaque watch bookmark and is never interpreted.

State integrity never rests on any of this: the actor runtime's storage etag CAS remains the floor, exactly as with every other provider.

Clocks

renewTime is written by each host's own clock and compared against the observer's, so peer freshness assumes NTP-synced nodes — the same assumption kubelet node Leases make. clockSkewMs (default 2 s) is the slack; raise it if your nodes drift more.

Scale

Every renewal is a watch event delivered to every host: n hosts beating every 5 s ≈ n²/5 events per second cluster-wide. At tens of hosts this is trivial (30 hosts ≈ 180 tiny JSON lines/s, and none of them touch the view). For hundreds of hosts, raise heartbeatMs/ttlMs — the volume falls quadratically.

RBAC

The ServiceAccount needs Lease access in the host namespace, nothing else:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
    name: sigx-actors-membership
rules:
    - apiGroups: ["coordination.k8s.io"]
      resources: ["leases"]
      verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
    name: sigx-actors-membership
roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: Role
    name: sigx-actors-membership
subjects:
    - kind: ServiceAccount
      name: my-host

Development outside the cluster

Kubeconfigs are deliberately not parsed (client certificates and exec plugins are a dependency magnet). Let kubectl do the auth instead:

kubectl proxy --port=8001
k8sMembership({ apiServer: 'http://127.0.0.1:8001', namespace: 'dev' });

The proxy authenticates; the provider talks plain localhost HTTP (with no ServiceAccount mount present, it simply sends no credentials).

Tests

The provider suite runs against an in-process fake API server — no cluster needed, pnpm test actors-k8s. The real-cluster lifecycle suite is gated on KUBECONFIG and drives a spawned kubectl proxy:

KUBECONFIG=~/.kube/config pnpm test actors-k8s