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

achievements

v0.3.0

Published

Type-safe, framework-agnostic achievement tracking library for web applications

Readme

achievements

Framework-agnostic achievement tracking with zero runtime dependencies.

npm bundle size License: MIT TypeScript

npm install achievements
# pnpm add achievements
# yarn add achievements
# bun add achievements

The engine is a plain TypeScript object. No framework, no context, no magic. You configure it once with your definitions and call methods like unlock(), setProgress(), or collectItem() from wherever it makes sense in your app. Persistence, progress tracking, anti-cheat, and toast queuing are all handled internally.

Looking for React bindings? See achievements-react.

Table of contents

Quick start

import { createAchievements, localStorageAdapter } from "achievements";

const engine = createAchievements({
  definitions: [
    { id: "first-visit", label: "First Visit", description: "Open the app." },
    { id: "click-frenzy", label: "Click Frenzy", description: "Click 50 times.", maxProgress: 50 },
  ],
  storage: localStorageAdapter("my-app"),
});

engine.subscribe((state) => {
  console.log("unlocked:", [...state.unlockedIds]);
});

engine.unlock("first-visit");
engine.incrementProgress("click-frenzy"); // auto-unlocks at 50

Defining achievements

Use defineAchievements to get literal-type inference on your IDs. The ID union is derived directly from your data, no manual type annotation needed.

import { defineAchievements } from "achievements";

export const definitions = defineAchievements([
  { id: "first-visit", label: "First Visit", description: "Open the app." },
  { id: "collector", label: "Collector", description: "Collect 10 items.", maxProgress: 10 },
  { id: "night-owl", label: "Night Owl", description: "Use the app after midnight.", hidden: true },
]);

// Free type, zero boilerplate
export type AchievementId = (typeof definitions)[number]["id"];
// => 'first-visit' | 'collector' | 'night-owl'

Definition fields

| Field | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------- | | id | string | Required. Unique identifier, inferred as a literal type. | | label | string | Required. Human-readable name for display. | | description | string | Required. Short description of the unlock condition. | | maxProgress | number | Enables progress tracking. Auto-unlocks when progress reaches this value. | | hidden | boolean | Hides the achievement entirely until unlocked. Default: false. | | hint | boolean | Hides only the description until unlocked. Default: false. |

Creating the engine

import { createAchievements, localStorageAdapter, fnv1aHashAdapter } from "achievements";

const engine = createAchievements({
  definitions,
  storage: localStorageAdapter("my-app"), // optional, default: localStorage (no prefix)
  hash: fnv1aHashAdapter(), // optional, default: FNV-1a (32-bit)
  onUnlock: (id) => console.log("Unlocked:", id),
  onTamperDetected: (key) => console.warn("Tamper detected:", key),
});

Config

| Option | Type | Default | Description | | ------------------ | ------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------ | | definitions | ReadonlyArray<AchievementDef<TId>> | - | Your achievement definitions. | | storage | StorageAdapter | localStorageAdapter() | Pluggable storage backend. | | hash | HashAdapter | fnv1aHashAdapter() | Hash function for tamper detection. | | onUnlock | (id: TId) => void | - | Called synchronously when an achievement unlocks. | | onTamperDetected | (key: string) => void | - | Called when stored data fails its integrity check. The entry is wiped automatically. |

Engine API

Writes

unlock(id)

Unlocks an achievement. No-op if already unlocked. Adds the ID to the toast queue and fires onUnlock.

engine.unlock("first-visit");

setProgress(id, value)

Sets progress to an absolute value. Clamped to [0, maxProgress]. Auto-unlocks when value >= maxProgress.

engine.setProgress("collector", 7);

incrementProgress(id)

Shorthand for setProgress(id, current + 1).

engine.incrementProgress("collector");

collectItem(id, item)

Adds a unique string to the achievement's item set, then calls setProgress(id, items.size). Idempotent: the same item can be passed multiple times safely.

engine.collectItem("explorer", "module-core");
engine.collectItem("explorer", "module-core"); // no-op
engine.collectItem("explorer", "module-react"); // progress: 2

setMaxProgress(id, max)

Updates maxProgress at runtime (in-memory only, not persisted). Immediately re-evaluates current progress and auto-unlocks if the threshold is already met. Useful when the target is only known after a data fetch.

// The definition has no maxProgress. We set it once we know the server count.
engine.setMaxProgress("full-coverage", serverNodeCount);

dismissToast(id)

Removes an ID from the toast queue. Call this after your UI has finished showing the notification.

engine.dismissToast("first-visit");

reset()

Wipes all in-memory state and removes all stored entries (unlocked set, progress, items, and their integrity hashes).

engine.reset();

Reads

All reads return synchronous snapshots of the current state.

| Method | Returns | Description | | -------------------- | ---------------------------------- | ----------------------------------------------------- | | isUnlocked(id) | boolean | Whether the achievement is unlocked. | | getProgress(id) | number | Current progress, or 0 if unset. | | getItems(id) | ReadonlySet<string> | Items collected via collectItem(). | | getUnlocked() | ReadonlySet<TId> | All currently unlocked IDs. | | getUnlockedCount() | number | Count of unlocked achievements. | | getDefinition(id) | AchievementDef<TId> \| undefined | The original definition object. | | getState() | AchievementState<TId> | Full state snapshot (unlocked, progress, toastQueue). |

if (engine.isUnlocked("first-visit")) {
  /* ... */
}

const { unlockedIds, progress, toastQueue } = engine.getState();

Reactivity

subscribe(listener) -> () => void

Registers a listener called after every mutation. Returns an unsubscribe function.

const unsubscribe = engine.subscribe((state) => {
  renderAchievementList(state.unlockedIds);
});

// Later, to stop listening:
unsubscribe();

Storage adapters

localStorageAdapter(prefix?)

Reads and writes window.localStorage. Keys are namespaced with an optional prefix to avoid collisions.

import { localStorageAdapter } from "achievements";

// Stored as "my-app:unlocked", "my-app:progress", etc.
const storage = localStorageAdapter("my-app");

SSR-safe. All window accesses are guarded.

inMemoryAdapter()

Stores data in a Map for the lifetime of the module. Great for tests or environments without localStorage.

import { inMemoryAdapter } from "achievements";

const storage = inMemoryAdapter();

Custom adapter

Implement StorageAdapter to plug in any backend: IndexedDB, a REST API, AsyncStorage, etc.

import type { StorageAdapter } from "achievements";

const myAdapter: StorageAdapter = {
  get(key) {
    return myStore.read(key);
  },
  set(key, value) {
    myStore.write(key, value);
  },
  remove(key) {
    myStore.delete(key);
  },
};

Anti-cheat & hash adapters

Every persisted entry is stored alongside an integrity hash. On hydration the hash is recomputed. If it doesn't match, onTamperDetected fires, the entry is wiped, and the engine starts from a clean slate.

The default algorithm is FNV-1a (32-bit): fast, synchronous, zero dependencies. To use a stronger function, pass a custom HashAdapter:

import type { HashAdapter } from "achievements";

const myHashAdapter: HashAdapter = {
  hash(data: string): string {
    return myCustomHash(data); // must be synchronous and deterministic
  },
};

const engine = createAchievements({ definitions, hash: myHashAdapter });

Note: Hashes live in localStorage as plain strings, so a determined user can still forge them. This is a friction layer, not cryptographic security.

TypeScript types

import type {
  AchievementDef,
  AchievementState,
  AchievementEngine,
  StorageAdapter,
  HashAdapter,
} from "achievements";

AchievementDef<TId>

The shape of a single definition object (see Definition fields).

AchievementState<TId>

The snapshot passed to subscribers and returned by getState():

type AchievementState<TId extends string> = {
  unlockedIds: ReadonlySet<TId>;
  progress: Readonly<Record<string, number>>;
  toastQueue: ReadonlyArray<TId>;
};

AchievementEngine<TId>

The full engine interface returned by createAchievements(). All methods are documented above.

StorageAdapter

type StorageAdapter = {
  get(key: string): string | null;
  set(key: string, value: string): void;
  remove(key: string): void;
};

HashAdapter

type HashAdapter = {
  hash(data: string): string;
};