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

@jsm-mit/chat-sharded-package

v0.4.0

Published

Wrapper TypeScript package for the chat group (coordinator, core, workers): projects, rooms, multi-party chats, messages and a poller for agent hosts. Takes the coordinator's id and finds the canister of every call.

Readme

@jsm-mit/chat-sharded-package

TypeScript wrapper for the chat group — a generic, multi-project, multi-party chat on the Internet Computer, spread over several canisters — plus the integration tests that prove it.

| Canister | Part of chat-sharded | Holds | |---|---|---| | coordinator | coordinator/ (Rust) | where each chat is, the workers and their load | | core | core/ (Motoko) | projects, rooms, admins | | workers | worker/ (Motoko) | chats and messages |

A consumer knows ONE id: the coordinator's. Every actor takes it and works as if the group were one canister: how many canisters hold the chats, and which one holds a chat, is the package's own business. No method of the main entry takes or returns the id of a worker.

import { RoomsActor, ChatsActor, MessagesActor, ChatPoller } from "@jsm-mit/chat-sharded-package";

// A canister admin creates the project with its accounts canister id — the consumer
// group's COORDINATOR, asked WHERE: before a non-admin opens a chat the core asks it
// `accountsCanisterFor(principal)` and asks `isUser(principal)` on the canister it names
// (Sultana: the sultana-sharded coordinator, which names its core).
// A project admin defines a room: who receives the customer's messages.
// One room per thing the app wants a conversation about (Sultana: per salon), closed to
// the people who may open it — an empty allowedCustomers means everybody with an account.
await new RoomsActor(coordinatorId, adminIdentity).createRoomAsyncUnsafe({
    projectId: "sultana-stage", id: `salon-${salonId}`, name: "Asystent salonu",
    recipients: [{ principal: villagePrincipal, role: "agent", displayName: "Asystentka Ola" }],
    allowedCustomers: [ownerPrincipal],
});

// The customer opens the room; the same call returns the same chat next time, whichever
// worker holds it.
const chats = new ChatsActor(coordinatorId, identity);
const chat = await chats.openChatAsyncUnsafe({ projectId: "sultana-stage", roomId: `salon-${salonId}`, customerName: "Michał" });

const messages = new MessagesActor(coordinatorId, identity);
await messages.postAsyncUnsafe({ chatId: chat.id, text: "dodaj usługę strzyżenie" });

// The page carries the thread AND what the others are doing: `presence[].typing` while
// somebody is writing (a room's recipients report it with setTypingAsyncUnsafe, and their
// own post clears it), `presence[].lastReadSeq` for "read up to here".
const page = await messages.getMessagesAsyncUnsafe({ chatId: chat.id, sinceSeq: 0 });

// A host that sends receipts and typing as a courtesy skips the proof: the call is taken
// in by the IC, the certified reply is never verified (that check is the expensive part
// on a small CPU), and a refusal goes unnoticed.
await messages.markReadUnverifiedAsyncUnsafe(chat.id, page.lastSeq);
await messages.setTypingUnverifiedAsyncUnsafe(chat.id);

// Every actor takes `{ blsVerify }` as a third argument: a faster check of the certified
// replies than agent-js's pure-JS one, for a host that makes many update calls.
const fast = new MessagesActor(coordinatorId, identity, { blsVerify: myVerifier });

// `{ host }` points an actor at another gateway or replica (default: mainnet, icp0.io). For a
// host on this machine — and only then — the agent fetches the replica's root key.
const local = new MessagesActor(localCoordinatorId, identity, { host: "http://127.0.0.1:4943" });

// An agent host: every new message in every chat this identity is a recipient of, each
// once, with the room and the customer on each message — route by (projectId, roomId).
const poller = new ChatPoller(messages, 1000, { onError: console.error });
poller.messages$.subscribe((m) => console.log(m.roomId, m.customer, m.kind, m.text));
poller.run();

// To go on after a restart where the poller stopped: keep its cursor (a text), hand it back.
const saved = poller.cursor;
const resumed = new ChatPoller(messages, 1000, { startAt: saved });

// The same by hand. No cursor = from the very start; { limit: 0 } = the cursor of now.
let { nextCursor } = await messages.pollNewMessagesAsyncUnsafe({ limit: 0 });
const news = await messages.pollNewMessagesAsyncUnsafe({ cursor: nextCursor });

Coming from @jsm-mit/chat-motoko-package 0.8

This package is the successor of the single-canister @jsm-mit/chat-motoko-package (its last version 0.8.0) and starts again at 0.1.0. What a consumer changes:

| chat-motoko-package 0.8 | chat-sharded-package | |---|---| | Every actor takes the id of THE chat canister. | Every actor takes the id of the coordinator. | | pollNewMessagesAsyncUnsafe({ sinceGlobalSeq }) → { messages, nextSinceGlobalSeq, headGlobalSeq } | pollNewMessagesAsyncUnsafe({ cursor }) → { messages, nextCursor, errors }. The cursor is a text to keep, not to read. | | ChatPoller.cursor: number, startAt: number | ChatPoller.cursor: string, startAt: string | | MessageView.globalSeq | gone. A message is named by chatId + seq; both stay when the chat moves. Use seq for order, keys and "seen up to". | | ChatSummaryView.messageCount, lastMessageAt | gone: the core holds no messages. | | — | new error keys: ChatIsMoving (a write during a move that outlasted the tries), NoWorkerAvailable (no worker takes new chats and none can be created — when one is being created, openChat waits for it, up to a minute). |

Env names on the consumers' side: CHAT_CANISTER_ID → CHAT_COORDINATOR_ID, VITE_CHAT_CANISTER_ID → VITE_CHAT_COORDINATOR_ID.

How a call finds its canister

  • The core (AdminActor, ProjectsActor, RoomsActor, the two admin methods of ChatsActor): its id is asked from the coordinator before the first call and kept.
  • A chat (getChat, post, markRead, setTyping, getMessages): the coordinator names the worker; the package keeps the answer until that worker says NotFound. Then the coordinator is asked again and the call goes to the worker it names now — a chat that moved is followed without the consumer seeing it. A write during the move waits (5 tries, 2 s apart).
  • A new chat (openChat): the coordinator names a worker that takes new chats; when the chat exists in another worker, that worker is named and the call goes there.
  • Lists over the group (getMyChats, pollNewMessages, ChatPoller): every worker is asked and ONE answer is made of it. One worker that does not answer fails getMyChats (a list is never silently short). A poll goes on with the others: the messages of the silent one come with a later poll, and what it failed with is in errors (ChatPoller hands it to onError).
  • The coordinator's answers are queries without a certificate. Every worker it names must be on its list of workers, and a worker refuses a chat it does not hold.

Actors built with the same identity object, coordinator and host share what they learned (ChatGroup). An identity that was renewed (a new delegation) is a new object and starts with nothing in hand.

The cursor of a poll

One text stands for the place in every worker and for the newest message read of every chat of the last ten minutes. So:

  • A worker that joins the group later is read from its start: everything in it is new.
  • A chat that moves brings its messages of the last two minutes along as news in the new worker (a reader may not have read them in the old one). The cursor knows what was read, and the poll brings each message ONCE — also after a restart, when the poller starts again with a cursor it kept.
  • Two limits. A poller that did not run for more than two minutes before a move misses what was written in that time. A poller that begins "from now" ({ limit: 0 }, or a ChatPoller without startAt) right after a move may get messages of that chat that are up to two minutes older than its start.
  • The messages of one chat come in the order of the chat; all of them about in the order of their time.

Classes

  • Main entry, for consumers: AdminActor, ProjectsActor, RoomsActor, ChatsActor, MessagesActor, ChatPoller.
  • @jsm-mit/chat-sharded-package/group, for the superadmin's scripts and for tests — the canisters one by one: CoordinatorActor (routing queries for everybody; config, the worker wasm, workers, moves and the status for superadmins), CoreAdminActor, CoreProjectsActor, CoreRoomsActor, CoreChatsActor, WorkerChatsActor, WorkerMessagesActor (a worker's own poll, with its globalSeq), ChatGroup, the mappers and the candid types.

Every method is …AsyncUnsafe (throws one of CanisterError, CriticalCanisterError, CallRefusedAtInspectionStage) or …AsyncSafe (never throws). Views translate Principal → text, opt → optional, variants → string unions; sequence numbers are plain numbers, timestamps bigint nanoseconds.

The coordinator is a Rust canister: it answers { Ok } | { Err }, and the package throws its Err as the same CanisterError (cause.errorKey = NotAuthorized, NotFound, AlreadyExists, InvalidData, NoCycles, RemoteCanisterError; no logs). An UPDATE of somebody who is neither a superadmin nor a worker is refused before it runs: CallRefusedAtInspectionStage.

Deleting: RoomsActor.deleteRoomAsyncUnsafe (project admins) takes a room with every chat and message in it; ProjectsActor.deleteProjectAsyncUnsafe (canister admins only) takes a project with all its rooms. The core deletes in bounded calls (200 chats each, purged in the workers that hold them), and these methods repeat the call until it is done — pass onStep to see the progress, or call deleteRoomStepAsyncUnsafe / deleteProjectStepAsyncUnsafe for one call at a time.

Usage billing (0.4.0, ../high-level-design/billing.md): ProjectsActor.getProjectBillAsyncUnsafe (project admins and the project's accounts canister) reads a project's account — the bill, its part per room, the usage not billed yet, the grace. AdminActor lists the accounts and sets the fees, the switch and the exempt flag (listBillingAccountsAsyncUnsafe, getBillingStatusAsyncUnsafe, setBillingSettingsAsyncUnsafe, setProjectExemptAsyncUnsafe); CoordinatorActor has the switch of the group, the thresholds of the multiplier and the daily run (getBillingAsyncUnsafe, setBillingEnabledAsyncUnsafe, setBillingThresholdsAsyncUnsafe, runBillingNowAsyncUnsafe). The payment itself, payProjectBill, carries cycles: it is a call of the payer canister, not of an agent, so the package has no method for it. A post in a project whose bill is unpaid for 14 days throws CanisterError with errorKey NotAuthorized (the text names the unpaid bill); reading goes on.

src/ is browser-safe — the same package serves the agent host and a web panel.

Scripts

build, typecheck, sync-declarations (-- worker | core | coordinator for one), test-unit, test-core-local, test-suite-local, test-routing-local, test-access-restrictions-local, test-smoke-local, user1 / user2 (an interactive admin/chat console, one person per terminal: the key comes from USER1_PEM / USER2_PEM in .env, the group from COORDINATOR_CANISTER_ID; another group: npm run user2 -- <coordinator-id>), stamp-canister, publish-public. See CLAUDE.md for who runs what.

Integration tests

The suites run on the local dfx network only, with no cycles and no lease — wrapper packages are never tested on mainnet (decision of the user, 2026-10-17), and a worker is installed by its coordinator only:

npm run test-suite-local        # or test-core-local, test-routing-local,
                                # test-access-restrictions-local, test-smoke-local

scripts/run-tests-local.sh builds a whole group there from the current builds of the three sibling repos (npm run local:group in the coordinator repo: a fresh coordinator, a fresh core, one worker the coordinator creates), makes the test superadmin (an identity of a public seed, tests/support/superadmin.ts) a superadmin of that coordinator, and runs the entry with COORDINATOR_CANISTER_ID, CORE_CANISTER_ID, CANISTER_ID (the first worker) and HOST. The actors under test take the coordinator's id only. The network must already run (dfx start --background > /tmp/dfx-local.log 2>&1 — never --clean: every project on the machine keeps its local canisters there).

npm run load-local loads one worker for a minute and moves a chat of a thousand messages (scripts/load.ts); it prints the worker's own account: instructions and bytes per message, messages in one round, the time of a move.

npm run load-dev runs the same test against the DEV group on mainnet (coordinator1 of the crafting table) as the dfx identity in use, a deploy admin there — the user runs it, it spends cycles. It needs exactly ONE worker in the group, gives the project a fresh id, puts the daily quota back and deletes the second worker with its chat at the end (its cycles go back to the coordinator; the create fee of 0.5 TC is burned). Before a run the coordinator holds 2 × cyclesPerWorker + 0.5 TC + cyclesPerWorker (4.5 TC with 1 TC). LOAD_CUSTOMERS, LOAD_PARALLEL, LOAD_SECONDS change the shape (both commands); LOAD_NO_MOVE=1 skips the move and the second worker — e.g. one post at a time: LOAD_CUSTOMERS=1 LOAD_PARALLEL=1 LOAD_NO_MOVE=1 npm run load-dev. LOAD_KEEP_QUOTA=1 leaves the daily quota alone — for a light probe started in a second terminal next to a heavy run (on DEV only: locally every run builds its own group). The script prints the CPU its client used: near 100% of one core, the client is the queue.

npm run load-users-local / npm run load-users-dev (scripts/load-users.ts): many independent users, as in life — each with its own identity and chat posts, waits for the answer, pauses (LOAD_PAUSE_S, 10 s on average) and posts again. The users run in child processes, one per core less one (LOAD_PROCS), so no client is the queue. Steps of users (LOAD_STEPS, default 50,100,200,400), each a whole minute and a quiet minute after it; for every step the users' p50/p95/p99/max, the failures, the busiest client's CPU and the worker's own minute. The daily quota is not touched.

npm run test-unit runs tests/unit/ — the router, the poller and the retry, with no canister and no network.

Publishing

Bump the version in package.json by hand, start the local network, then:

npm run publish-public

scripts/publish-public.sh refuses unless this repo is on master equal to origin/master with nothing uncommitted but the bump, the version is not on the registry yet, and the three canister repos are on master, clean and equal to their origin/master. It then builds a group from those masters on the local network, reads the interface each canister answers with (candid:service) and refuses if one differs from the committed declarations/ (scripts/check-interfaces.sh; comments, white space and install arguments aside) — sync the declarations, commit, push, publish again. On the same group it runs the smoke test (tests/test-smoke.ts) and refuses if it fails. Next scripts/stamp-canister-commit.sh repeats the repo checks, rebuilds the canisters, regenerates the declarations and refuses if that changes what is committed. Only then it writes the three stamps into package.json — chatMotoko, chatCoreMotoko, chatPlatformCanisterCoordinator, each {repo, repoUrl, commit} — publishes, and commits and pushes Version bump to <version> (when there is nothing to commit, it stops there without an error). A failed publish takes the stamps back out.