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

@october1/client

v0.1.0

Published

A small client for october: GraphQL reads and writes that retry safely, live queries, sign-in, and React hooks.

Readme

@october1/client

A small client for one october tenant: GraphQL reads and writes, live queries, and sign-in by email code. No runtime dependencies; about 7 KB gzipped.

import { init } from "@october1/client";

const db = init({ url: "https://october.example", tenant: "shop" });
// same origin as the page: init({ tenant: "shop" })

// sign in
const { demoCode } = await db.auth.sendMagicCode({ email });
await db.auth.signInWithMagicCode({ email, code });

// read
const { products } = await db.query(`{ products(first: 20) { items { id name } } }`);

// write: every call carries an Idempotency-Key, so retries never write twice
await db.mutate(`mutation ($input: ProductCreateInput!) { createProduct(input: $input) { id } }`,
  { input: { name: "Lamp" } });

// live: the first result, then every change
const stop = db.subscribe(`{ products(first: 20) { items { id name } } }`, {}, ({ data, mine }) => render(data));

React

import { init, id } from "@october1/client/react";

const db = init({ tenant: "chat" });
const MESSAGES = `{ messages(first: 50) { items { id text createdAt } } }`;
const SEND = `mutation ($input: MessageCreateInput!) { createMessage(input: $input) { id } }`;

function Chat() {
  const { isLoading, error, data } = db.useQuery(MESSAGES);   // live: re-renders on every change
  const { user } = db.useAuth();
  const send = (text) => db.mutate(SEND, { input: { id: id(), text } });
  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>{error.message}</p>;
  return <UI messages={data.messages.items} canSend={!!user} onSend={send} />;
}
  • db.useQuery(doc, variables, { skip }) answers {isLoading, data, error, mine, basisT}. A null doc runs nothing. Components asking for the same query and variables share one live stream (the server allows a user 8), and the stream closes when the last of them unmounts.
  • db.useAuth() answers {isLoading, user, token} and follows sign-in, sign-out and other tabs.
  • id() makes a new entity id. Creates take input.id, so a component can name what it creates before the server answers.

React is an optional peer dependency. The core (@october1/client) needs nothing.

Writes are retried safely

mutate sends an Idempotency-Key header (a random one per call, or { idempotencyKey }). If the request times out, the network fails, or the server answers UNAVAILABLE, CONFLICT or RATE_LIMITED, the client sends the same request again with the same key. If the write committed the first time, the server answers with that commit and does not write again. Reads are retried the same way, without a key.

Retries wait for the server's retryAfterMs, else a short backoff. They give up after attempts tries (4), or when the server asks for a wait longer than maxRetryWaitMs (10 s).

Errors

Every failure is an OctoberError:

  • code: the GraphQL extensions.code (VALIDATION_FAILED, FORBIDDEN, NOT_FOUND, UNAUTHENTICATED, CONFLICT, UNAVAILABLE, RATE_LIMITED, ...). For a REST refusal, its type in upper snake case (INVALID_CODE, RATE_LIMITED, UNKNOWN_TENANT). Else NETWORK or TIMEOUT.
  • fields: for VALIDATION_FAILED, {field: [message]}.
  • retryAfterMs, status, details (the rest of the error).
  • data: what did commit. Each mutation field in a request is its own transaction.
  • retryable.

db.request(doc, vars) returns the whole response (data, errors, extensions.cost) instead of throwing on GraphQL errors.

Live queries

db.subscribe(doc, variables, callback, { onStatus, onError }) calls callback with {data, errors, basisT, mine, cost}. mine is true when every write in the push was the signed-in user's own. The client:

  • gets a live ticket when signed in;
  • reconnects with backoff, and when the stream goes quiet (no heartbeat for liveIdleMs, 45 s);
  • reopens every stream as the new caller when the sign-in changes.

A query the server refuses (a parse error, a mutation, over the cost limits) calls onError and the subscription stops. It runs over fetch, so it works in Node too.

Sign-in

The token is kept in localStorage under october:<tenant>:token (tokenKey, storage; storage: null keeps it in memory). Another tab's sign-in or sign-out is picked up.

  • db.auth.token, db.auth.onChange(fn), db.auth.setToken(t).
  • db.auth.me() answers {id, email} or null. A token the server no longer knows is forgotten.
  • db.auth.signOut() revokes the token on the server. Its live streams end.
  • db.config() answers {demo}.

Types (gql.tada)

Documents are strings or typed documents: gql.tada's, or any TypedDocumentNode. Results and variables are then typed from the tenant's SDL:

curl -fsS https://october.example/t/shop/schema.graphql -o schema.graphql
import { initGraphQLTada } from "gql.tada";
import type { introspection } from "./graphql-env.d.ts";
import { init, type Scalars } from "@october1/client";

const graphql = initGraphQLTada<{ introspection: introspection; scalars: Scalars }>();
const PRODUCT = graphql(`query ($id: UUID!) { product(id: $id) { id name } }`);
const { product } = await db.query(PRODUCT, { id }); // typed

Scalars maps october's scalars: UUID and Instant are strings, Long is a number or a decimal string, JSON is unknown.

Development

npm install
npm test           # unit tests, plus test/server.test.ts against OCTOBER_URL
                   # (default localhost:8900, the dev JVM; skipped when it isn't up)
npm run typecheck  # includes test/types.ts, typed from test/forum.graphql
npm run build      # dist/, and resources/public/october-client.js for october's own pages
npm run forum-types  # refresh test/forum.graphql and its types from the dev server

october's pages load /october-client.js, a copy that npm run build writes. The copy's header names the hash of the source it was built from, and october.client-copy-test fails when the copy is stale.