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

convex-timeline

v0.1.2

Published

A timeline component for Convex.

Readme

Convex Timeline

A Convex component for undo/redo state management with named checkpoints.

Overview

Timeline maintains a linear history of state snapshots organized by scope. It provides:

  • Undo/Redo: Navigate backward and forward through state history
  • Checkpoints: Named snapshots that persist independently of the timeline
  • Automatic Pruning: Configurable limits to prevent unbounded growth

https://github.com/user-attachments/assets/22bb5e41-89ac-4273-9bb7-7ab298e5d012

Read the How It Works section for details on the pruning strategy and possible future improvements.

Installation

npm install convex-timeline
// convex/convex.config.ts
import { defineApp } from "convex/server";
import timeline from "convex-timeline/convex.config.js";

const app = defineApp();
app.use(timeline);
export default app;

Usage

import { Timeline } from "convex-timeline";
import { components } from "./_generated/api";

const timeline = new Timeline(components.timeline);

// Use directly with scope
await timeline.push(ctx, "doc:123", { text: "Hello" });
await timeline.undo(ctx, "doc:123");

// Or create a scoped facade
const doc = timeline.forScope("doc:123");
await doc.push(ctx, { text: "Hello" });
await doc.undo(ctx);

See example/convex for a full example.

https://github.com/user-attachments/assets/75f3a2da-f2b4-43c2-aade-aa537afb5bb3

Hosted version

Constructor Options

// Unlimited history (default)
new Timeline(components.timeline);

// Global limit
new Timeline(components.timeline, { maxNodesPerScope: 100 });

// Per-prefix limits (longest match wins)
new Timeline(components.timeline, {
  maxNodesPerScope: { "doc:": 200, "scratch:": 50 },
});

API

All methods are available both on Timeline (with scope parameter) and on the scoped facade from forScope().

Timeline Operations

| Method | Description | | ----------------------------- | --------------------------------------------------------- | | push(ctx, scope, state) | Push new state. Prunes nodes ahead of head if after undo. | | undo(ctx, scope, count?) | Move head back. Returns state or null if at start. | | redo(ctx, scope, count?) | Move head forward. Returns state or null if at end. | | currentDocument(ctx, scope) | Get current state without modifying timeline. | | status(ctx, scope) | Returns { canUndo, canRedo, position, length }. | | clear(ctx, scope) | Remove all nodes, reset head. Preserves checkpoints. | | deleteScope(ctx, scope) | Delete scope and all data including checkpoints. |

Checkpoints

Checkpoints are named snapshots stored independently—they persist even when nodes are pruned.

| Method | Description | | ----------------------------------------- | ---------------------------------------------- | | createCheckpoint(ctx, scope, name) | Save current state as named checkpoint. | | restoreCheckpoint(ctx, scope, name) | Push checkpoint state as new node (undoable). | | getCheckpointDocument(ctx, scope, name) | Get checkpoint state without restoring. | | listCheckpoints(ctx, scope) | List all checkpoints with names and positions. | | deleteCheckpoint(ctx, scope, name) | Delete a checkpoint. |

Inspection

| Method | Description | | ---------------------------------------- | --------------------------------------------------- | | getDocumentAtPosition(ctx, scope, pos) | Get state at specific position without moving head. | | listNodes(ctx, scope) | List all nodes with positions and documents. |

How It Works

After three pushes:
    [A] → [B] → [C]
                head

After undo:
    [A] → [B] → [C]
          head

After push(D) — C is pruned:
    [A] → [B] → [D]
                head

This pruning behavior matches the standard undo/redo model used by editors like Google Docs, VSCode, and Notion, where pushing new state after an undo discards the forward history. Future versions may support alternative models such as manual branching, time travel, and reconciliation strategies, depending on community interest.

Checkpoints persist through pruning:

createCheckpoint("v1") at C → saves C's state
After C is pruned → checkpoint "v1" still holds C
restoreCheckpoint("v1") → pushes C as new node (undoable)

Example

See example/ for a complete todo app with undo/redo.

cd example && npm install && npm run dev

FAQ

Why do we prune the redo branch?

I looked at many text editing programs (Google docs, VSCode, Notion, etc) and mimicked their behavior. When we undo and push, the redo branch can not be accessed again so we prune it to save on storage space.

Why are checkpoints stored in a separate table?

Consider the case where we checkpoint a node that gets pruned, either because the scope reaches capacity or it's part of the redo branch. In this case, we can no longer restore to that state if we stored the checkpoint as a property of the node.

Why does restoring a checkpoint create a new node instead of traveling back to it?

The original node might not exist anymore if it was pruned. Even if it does exist, creating a new node makes the restore undoable, meaning you can go back to where you were before restoring. It also protects the restored state from being pruned if you undo and push new state later. There is a future where I introduce a time-travel mode if there is demand for it.

Why not use the Convex transaction log?

Ideally the Convex transaction log (described here) can be used so we can specify pointers to data (obviously this would be more complicated since we'd need to consider indexes) but unfortunately Convex does not currently expose the transaction log.

In addition to this, it's not necessary the case that the timeline component is only used for tracking documents in tables. It can be used for any data!

License

MIT