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

@ws-asyncapi/cursors

v0.1.0

Published

Opt-in live-cursors helper for ws-asyncapi — a smoothed, framework-agnostic store over presence (works with the react/solid bindings via useStore/fromStore)

Readme

@ws-asyncapi/cursors

Opt-in live cursors for ws-asyncapi — show everyone's pointer in real time. It adds no protocol and no opinion to the core libs: it's a thin, smoothed projection over the general presence machinery. You send with the general volatile primitive (client.presence.update); this package turns the incoming presence roster into a smoothed Map<id, {x,y}> of everyone else's cursor for you to render.

Install

npm install @ws-asyncapi/cursors          # peer: @ws-asyncapi/query-core
# perfect-cursors is OPTIONAL (a drop-in smoother), never required

You also need a framework binding (@ws-asyncapi/react or @ws-asyncapi/solid) and the server (@ws-asyncapi/adapter-node or -elysia).

Quick start

1. Put a cursor field in your channel's presence schema (your names, your shape):

// server.ts
import { Channel } from "ws-asyncapi";
import { z } from "zod";

export const board = new Channel("/board/:id", "board").presence(
  z.object({
    name: z.string(),
    color: z.string(),
    cursor: z.object({ x: z.number(), y: z.number() }).nullable(),
  }),
);
// serve it with createNodeWsServer([board]) / wsAsyncAPIAdapter([board])

2. Announce yourself, stream your cursor, render the others (React):

// Board.tsx
import { useEffect, useMemo } from "react";
import { createReactClient, useStore } from "@ws-asyncapi/react";
import { cursorsStore } from "@ws-asyncapi/cursors";
import type { board } from "./server";

// one client per app; coalesce cursor moves to ~20Hz on the wire
const ws = createReactClient<typeof board>("ws://localhost:3000", "/board/1", {
  presenceThrottle: 50,
});

export function Board() {
  useEffect(() => {
    // join the roster once…
    ws.client.presence.set({ name: "Alice", color: "#e11d48", cursor: null });
    // …then stream the cursor (volatile, throttled, dropped while offline)
    const onMove = (e: PointerEvent) =>
      ws.client.presence.update({ cursor: { x: e.clientX, y: e.clientY } });
    window.addEventListener("pointermove", onMove);
    return () => window.removeEventListener("pointermove", onMove);
  }, []);

  // others' smoothed cursors (yourself excluded)
  const cursors = useStore(useMemo(() => cursorsStore(ws.client), []));

  return (
    <>
      {[...cursors].map(([id, { x, y }]) => (
        <svg
          key={id}
          width="24"
          height="24"
          style={{
            position: "fixed",
            left: 0,
            top: 0,
            transform: `translate(${x}px, ${y}px)`,
            pointerEvents: "none",
          }}
        >
          <path d="M0 0 L0 18 L5 13 L9 21 L12 19 L8 12 L15 12 Z" fill="black" />
        </svg>
      ))}
    </>
  );
}

That's the whole feature. cursors re-renders ~60fps with interpolated positions even though the wire rate is ~20Hz.

Solid

Same store, bound with fromStore:

import { fromStore } from "@ws-asyncapi/solid";
import { cursorsStore } from "@ws-asyncapi/cursors";

const cursors = fromStore(cursorsStore(ws.client)); // Accessor<Map<id, {x,y}>>
// render with <For each={[...cursors()]}>{([id, p]) => ...}</For>

Labels, colors, avatars

cursorsStore returns positions only (Map<id, {x,y}>). The rest of each member's state lives in presence — read it with usePresence() and join by socket id:

const cursors = useStore(useMemo(() => cursorsStore(ws.client), []));
const { members } = ws.usePresence(); // Map<id, { name, color, cursor }>

{[...cursors].map(([id, { x, y }]) => {
  const m = members.get(id);
  return <Cursor key={id} x={x} y={y} name={m?.name} color={m?.color} />;
})}

Smoothing

Default is a zero-dependency rAF lerp (smooth 60fps render from a ~20Hz wire rate; degrades to passthrough when there's no requestAnimationFrame, e.g. SSR). Swap it via smoothing:

cursorsStore(ws.client, { smoothing: false });                        // raw last-write
import { PerfectCursor } from "perfect-cursors";                      // your install
cursorsStore(ws.client, { smoothing: (cb) => new PerfectCursor(cb) }); // best-in-class spline

perfect-cursors already implements the smoother interface ({ addPoint([x,y]); dispose() }), so it drops in without being a dependency here.

Choosing the cursor field

The default reads state.cursor. Point it elsewhere (type-checked against your presence schema):

cursorsStore(ws.client, { field: (s) => s.pointer });

Return null/undefined to omit a member (e.g. their pointer left the canvas) — set cursor: null via presence.update({ cursor: null }) and they drop out of the map.

Coordinates

{x,y} are whatever you put in — this package never transforms them. For a plain web page, screen coords (clientX/clientY) are fine. For a pan/zoom canvas send document/scene coordinates and apply each viewer's transform on render; for very different screen sizes, send normalized coords (e.g. vw/vh fractions). That choice is yours and stays in your presence schema.

How it updates

client.presence.update({ cursor }) → volatile diff → server fan-out → the receiver's cursorsStore feeds each peer's point into its smoother → the smoother's rAF loop emits interpolated points → a fresh Map snapshot → useStore/fromStore re-renders. Yourself is excluded; peers that leave or null their cursor are removed and their animation loop disposed. Cursors are ephemeral — never persisted, never replayed on reconnect.

Why a separate package

The general libs (@ws-asyncapi/client, query-core, react, solid) stay free of any cursor-shaped assumption. Cursors ride entirely on two existing seams — the client's presence surface and query-core's Subscribable contract — bound to your framework with its generic store hook (useStore / fromStore). Nothing here is in the hot path of apps that don't use cursors.

License

MIT