@jsm-mit/chat-motoko-package
v0.8.0
Published
Wrapper TypeScript package for the chat-motoko canister: projects, multi-party chats, messages and a poller for agent hosts.
Downloads
45
Readme
@jsm-mit/chat-motoko-package
TypeScript wrapper for the chat-motoko canister — a generic,
multi-project, multi-party chat on the Internet Computer — plus the integration tests
that prove it.
import { RoomsActor, ChatsActor, MessagesActor, ChatPoller } from "@jsm-mit/chat-motoko-package";
// A canister admin creates the project with its accounts canister — the one asked
// `isUser(principal)` before a non-admin opens a chat (Sultana: sultana-core-motoko).
// 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(canisterId, 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.
const chat = await new ChatsActor(canisterId, identity).openChatAsyncUnsafe({ projectId: "sultana-stage", roomId: `salon-${salonId}`, customerName: "Michał" });
const messages = new MessagesActor(canisterId, 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(canisterId, identity, { blsVerify: myVerifier });
// An agent host: every new message in every chat this identity is a recipient of, 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();One class per canister controller (AdminActor, ProjectsActor, RoomsActor,
ChatsActor, MessagesActor), every method …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.
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 canister deletes in bounded calls (500 chats or 50 000
messages each), 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.
src/ is browser-safe — the same package serves the agent host and a web panel.
Scripts: build, typecheck, sync-declarations, test-suite,
test-access-restrictions, test-smoke, user1 / user2 (an interactive admin/chat console, one
person per terminal: the key comes from USER1_PEM / USER2_PEM in .env; another
canister: npm run user2 -- <canister-id>), redeploy. See CLAUDE.md for who runs what.
Integration tests and redeploys
The tests always run on the crafting-table slot short-living, the short-lived test canister the
wrapper packages share. npm run test-suite (or test-access-restrictions, or test-smoke —
the quick check publishing runs) reinstalls the
current ../chat-motoko build into short-living with a 3-minute lease
(npm run craft:deploy -- short-living --lease 3 there), then runs the suite with CANISTER_ID set to short-living's id from
~/motoko-crafting-table/canister_ids.json. No prompt. The crafting table refuses while
another deploy's lease on short-living is active; npm run test-suite -- --force takes it anyway.
No .env: every identity is generated per run; setup claims the admin role on the fresh canister.
npm run redeploy -- [slot] [reinstall|upgrade] [--force]deploys the current ../chat-motoko build for sandbox play, without tests and without
.env: craft:deploy (reinstall) or craft:upgrade in ../chat-motoko. The slot defaults
to backend6; the mode to upgrade where the slot allows it and to reinstall on short-living.
After a reinstall claim the admin role yourself in the sandbox — the script prints the command.
Publishing
Bump the version in package.json by hand, then:
npm run publish-publicscripts/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 ../chat-motoko is on master, clean and equal to its origin/master. It then
reinstalls that master into short-living (craft:deploy -- short-living --lease 2), reads the live
interface (dfx canister metadata <short-living> candid:service --ic --identity anonymous) and
refuses if it differs from the committed declarations/chat-motoko-backend/chat-motoko-backend.did
(trailing whitespace and newlines aside) — sync the declarations, commit, push, publish
again. On the same deploy it runs the smoke test (tests/test-smoke.ts: setup, one write
and one read per domain) and refuses if it fails. Next scripts/stamp-canister-commit.sh repeats the canister-repo checks, rebuilds
the canister, regenerates the declarations and refuses if that changes what is committed
here. Only then it writes chatMotoko.{repo, repoUrl, commit} into package.json — the
canister commit this version was generated from — 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 stamp back out.
