{"_id":"convex-timeline","_rev":"3-f5d6c85136380d8b39e615aa25c9e077","name":"convex-timeline","dist-tags":{"alpha":"0.1.1-alpha.0","latest":"0.1.2"},"versions":{"0.1.1-alpha.0":{"name":"convex-timeline","version":"0.1.1-alpha.0","keywords":["convex","component","timeline","undo","redo","checkpoint"],"license":"Apache-2.0","_id":"convex-timeline@0.1.1-alpha.0","maintainers":[{"name":"meshankhosla","email":"meshankhosla@gmail.com"}],"homepage":"https://github.com/MeshanKhosla/convex-timeline#readme","bugs":{"url":"https://github.com/MeshanKhosla/convex-timeline/issues"},"dist":{"shasum":"ba548391ea73ac2343faed9a8006153c71a7253f","tarball":"https://registry.npmjs.org/convex-timeline/-/convex-timeline-0.1.1-alpha.0.tgz","fileCount":53,"integrity":"sha512-C/D4/t63+172gLYlH+GZ2SG+Dfi+N9FKQ18t7nNQVJckEiQOZ3lEYB7/anPNPD1l/rLl1IbE7gJHejnMF6hBlw==","signatures":[{"sig":"MEQCIB4EhVwdlVwXlXbFeoAW0rLlx+b5R/1nvSdcXztRtqFBAiBeAiYJgHnSon5hDJvpDvg8vWoSCY/JAY2Ro7EZzECdrw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":212126},"type":"module","types":"./dist/client/index.d.ts","module":"./dist/client/index.js","exports":{".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./package.json":"./package.json","./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"}},"gitHead":"55dd04aaa7221236a0b0f9468d74780dcf29a1c0","scripts":{"all":"run-p -r 'dev:*' 'test:watch'","dev":"run-p -r 'dev:*'","lint":"eslint .","test":"vitest run --typecheck","alpha":"npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags","build":"tsc --project ./tsconfig.build.json","clean":"rm -rf dist *.tsbuildinfo","format":"prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"","predev":"path-exists .env.local || (npm run build && convex dev --until-success)","release":"npm version patch && npm publish && git push --follow-tags","version":"vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npx convex codegen --component-dir ./src/component && npm run build' --initial","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","preversion":"npm run clean && npm ci && npm run build && run-p test lint typecheck","test:debug":"vitest --inspect-brk --no-file-parallelism","test:watch":"vitest --typecheck --clearScreen false","dev:backend":"convex dev --typecheck-components","dev:frontend":"cd example && vite --clearScreen false","format:check":"prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"","test:coverage":"vitest run --coverage --coverage.reporter=text","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"meshankhosla","email":"meshankhosla@gmail.com"},"repository":{"url":"git+https://github.com/MeshanKhosla/convex-timeline.git","type":"git"},"_npmVersion":"10.9.2","description":"A timeline component for Convex.","directories":{},"_nodeVersion":"22.17.1","_hasShrinkwrap":false,"devDependencies":{"vite":"6.4.1","react":"^19.2.0","convex":"1.29.3","eslint":"9.39.1","vitest":"3.2.4","cpy-cli":"^6.0.0","globals":"^16.5.0","prettier":"3.6.2","react-dom":"^19.2.0","@eslint/js":"9.39.1","pkg-pr-new":"^0.0.60","typescript":"5.9.3","@types/node":"^20.19.25","convex-test":"0.0.40","@types/react":"^19.2.7","chokidar-cli":"3.0.0","npm-run-all2":"8.0.4","path-exists-cli":"2.0.0","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.1","@types/react-dom":"^19.2.3","typescript-eslint":"8.47.0","eslint-plugin-react":"^7.37.5","@vitejs/plugin-react":"^5.1.1","@convex-dev/eslint-plugin":"^1.0.0","eslint-plugin-react-hooks":"^7.0.1","eslint-plugin-react-refresh":"^0.4.24"},"peerDependencies":{"convex":"^1.29.3"},"_npmOperationalInternal":{"tmp":"tmp/convex-timeline_0.1.1-alpha.0_1764897804198_0.4486737118520676","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"convex-timeline","version":"0.1.1","keywords":["convex","component","timeline","undo","redo","checkpoint"],"license":"Apache-2.0","_id":"convex-timeline@0.1.1","maintainers":[{"name":"meshankhosla","email":"meshankhosla@gmail.com"}],"homepage":"https://github.com/MeshanKhosla/convex-timeline#readme","bugs":{"url":"https://github.com/MeshanKhosla/convex-timeline/issues"},"dist":{"shasum":"c0d4d67ca7272516468ea6f009e89ca0b7c52fa0","tarball":"https://registry.npmjs.org/convex-timeline/-/convex-timeline-0.1.1.tgz","fileCount":53,"integrity":"sha512-3+64/5wVO6P4DIOQMBF+XhkZDlLnj+BsH4oUkTRkt+CByTtiYm+81IYK+BYdAeQGrujjQgbT9630z8M6oknvYA==","signatures":[{"sig":"MEYCIQC91nKe0fUSVtshcGf8AfCUaSBcNp3YvBD2CfJHHkpKgQIhAMBIMcUl3IyG5x6NTMkDFHthgXryKxyrqvJmCAPylYUe","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":212254},"type":"module","types":"./dist/client/index.d.ts","module":"./dist/client/index.js","exports":{".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./package.json":"./package.json","./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"}},"gitHead":"17869e34b130bc5bdb034f71f73352083e200f9f","scripts":{"all":"run-p -r 'dev:*' 'test:watch'","dev":"run-p -r 'dev:*'","lint":"eslint .","test":"vitest run --typecheck","alpha":"npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags","build":"tsc --project ./tsconfig.build.json","clean":"rm -rf dist *.tsbuildinfo","format":"prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"","predev":"path-exists .env.local || (npm run build && convex dev --until-success)","release":"npm version patch && npm publish && git push --follow-tags","version":"vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npx convex codegen --component-dir ./src/component && npm run build' --initial","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","preversion":"npm run clean && npm ci && npm run build && run-p test lint typecheck","test:debug":"vitest --inspect-brk --no-file-parallelism","test:watch":"vitest --typecheck --clearScreen false","dev:backend":"convex dev --typecheck-components","dev:frontend":"cd example && vite --clearScreen false","format:check":"prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"","test:coverage":"vitest run --coverage --coverage.reporter=text","prepublishOnly":"npm run clean && npm run build"},"_npmUser":{"name":"meshankhosla","email":"meshankhosla@gmail.com"},"repository":{"url":"git+https://github.com/MeshanKhosla/convex-timeline.git","type":"git"},"_npmVersion":"10.9.2","description":"A timeline component for Convex.","directories":{},"_nodeVersion":"22.17.1","_hasShrinkwrap":false,"devDependencies":{"vite":"6.4.1","react":"^19.2.0","convex":"1.29.3","eslint":"9.39.1","vitest":"3.2.4","cpy-cli":"^6.0.0","globals":"^16.5.0","prettier":"3.6.2","react-dom":"^19.2.0","@eslint/js":"9.39.1","pkg-pr-new":"^0.0.60","typescript":"5.9.3","@types/node":"^20.19.25","convex-test":"0.0.40","@types/react":"^19.2.7","chokidar-cli":"3.0.0","npm-run-all2":"8.0.4","path-exists-cli":"2.0.0","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.1","@types/react-dom":"^19.2.3","typescript-eslint":"8.47.0","eslint-plugin-react":"^7.37.5","@vitejs/plugin-react":"^5.1.1","@convex-dev/eslint-plugin":"^1.0.0","eslint-plugin-react-hooks":"^7.0.1","eslint-plugin-react-refresh":"^0.4.24"},"peerDependencies":{"convex":"^1.29.3"},"_npmOperationalInternal":{"tmp":"tmp/convex-timeline_0.1.1_1764910079877_0.4423090933546807","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"convex-timeline","description":"A timeline component for Convex.","repository":{"type":"git","url":"git+https://github.com/MeshanKhosla/convex-timeline.git"},"homepage":"https://github.com/MeshanKhosla/convex-timeline#readme","bugs":{"url":"https://github.com/MeshanKhosla/convex-timeline/issues"},"version":"0.1.2","license":"Apache-2.0","keywords":["convex","component","timeline","undo","redo","checkpoint"],"type":"module","scripts":{"dev":"run-p -r 'dev:*'","dev:backend":"convex dev --typecheck-components","dev:frontend":"cd example && vite --clearScreen false","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npx convex codegen --component-dir ./src/component && npm run build' --initial","predev":"path-exists .env.local || (npm run build && convex dev --until-success)","clean":"rm -rf dist *.tsbuildinfo","build":"tsc --project ./tsconfig.build.json","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","lint":"eslint .","format":"prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"","format:check":"prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"","all":"run-p -r 'dev:*' 'test:watch'","test":"vitest run --typecheck","test:watch":"vitest --typecheck --clearScreen false","test:debug":"vitest --inspect-brk --no-file-parallelism","test:coverage":"vitest run --coverage --coverage.reporter=text","prepublishOnly":"npm run clean && npm run build","preversion":"npm run clean && npm ci && npm run build && run-p test lint typecheck","alpha":"npm version prerelease --preid alpha && npm publish --tag alpha && git push --follow-tags","release":"npm version patch && npm publish && git push --follow-tags","version":"vim -c 'normal o' -c 'normal o## '$npm_package_version CHANGELOG.md && prettier -w CHANGELOG.md && git add CHANGELOG.md"},"exports":{"./package.json":"./package.json",".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"}},"peerDependencies":{"convex":"^1.31.0"},"devDependencies":{"@convex-dev/eslint-plugin":"^1.1.0","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.1","@eslint/js":"9.39.1","@types/node":"^20.19.25","@types/react":"^19.2.7","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^5.1.1","chokidar-cli":"3.0.0","convex":"1.31.0","convex-test":"0.0.40","cpy-cli":"^6.0.0","eslint":"9.39.1","eslint-plugin-react":"^7.37.5","eslint-plugin-react-hooks":"^7.0.1","eslint-plugin-react-refresh":"^0.4.24","globals":"^16.5.0","npm-run-all2":"8.0.4","path-exists-cli":"2.0.0","pkg-pr-new":"^0.0.60","prettier":"3.6.2","react":"^19.2.0","react-dom":"^19.2.0","typescript":"5.9.3","typescript-eslint":"8.47.0","vite":"6.4.1","vitest":"3.2.4"},"types":"./dist/client/index.d.ts","module":"./dist/client/index.js","_id":"convex-timeline@0.1.2","gitHead":"3c5a1740eb2e5107d0837adad7b213ccf036a01c","_nodeVersion":"22.17.1","_npmVersion":"10.9.2","dist":{"integrity":"sha512-xT6dh1ohVUsDb/aepAKcvN9bcYGwu3tZ4Hxk1MrnzrcltHyId7EnhDij6piVA8JJRvBE1v9u8Rl6ugi9XKU+3A==","shasum":"f659dd81d2dc89a3183f53226f834dfdd35703a4","tarball":"https://registry.npmjs.org/convex-timeline/-/convex-timeline-0.1.2.tgz","fileCount":53,"unpackedSize":214325,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDWUJfe3ddfIh2zLGOfuFo7D6sMQ1ikWNsDxpe5M719YwIgdyTxiZOwqalrshavv/86ba0TtRNqEFXlMHwgTwi3f5w="}]},"_npmUser":{"name":"meshankhosla","email":"meshankhosla@gmail.com"},"directories":{},"maintainers":[{"name":"meshankhosla","email":"meshankhosla@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convex-timeline_0.1.2_1765857440376_0.25703184870430573"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-05T01:23:24.197Z","modified":"2025-12-16T03:57:20.718Z","0.1.1-alpha.0":"2025-12-05T01:23:24.336Z","0.1.1":"2025-12-05T04:48:00.086Z","0.1.2":"2025-12-16T03:57:20.518Z"},"bugs":{"url":"https://github.com/MeshanKhosla/convex-timeline/issues"},"license":"Apache-2.0","homepage":"https://github.com/MeshanKhosla/convex-timeline#readme","keywords":["convex","component","timeline","undo","redo","checkpoint"],"repository":{"type":"git","url":"git+https://github.com/MeshanKhosla/convex-timeline.git"},"description":"A timeline component for Convex.","maintainers":[{"name":"meshankhosla","email":"meshankhosla@gmail.com"}],"readme":"# Convex Timeline\n\nA Convex component for undo/redo state management with named checkpoints.\n\n## Overview\n\nTimeline maintains a linear history of state snapshots organized by scope. It\nprovides:\n\n- **Undo/Redo**: Navigate backward and forward through state history\n- **Checkpoints**: Named snapshots that persist independently of the timeline\n- **Automatic Pruning**: Configurable limits to prevent unbounded growth\n\nhttps://github.com/user-attachments/assets/22bb5e41-89ac-4273-9bb7-7ab298e5d012\n\nRead the\n[How It Works](https://github.com/MeshanKhosla/convex-timeline?tab=readme-ov-file#how-it-works)\nsection for details on the pruning strategy and possible future improvements.\n\n## Installation\n\n```sh\nnpm install convex-timeline\n```\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport timeline from \"convex-timeline/convex.config.js\";\n\nconst app = defineApp();\napp.use(timeline);\nexport default app;\n```\n\n## Usage\n\n```ts\nimport { Timeline } from \"convex-timeline\";\nimport { components } from \"./_generated/api\";\n\nconst timeline = new Timeline(components.timeline);\n\n// Use directly with scope\nawait timeline.push(ctx, \"doc:123\", { text: \"Hello\" });\nawait timeline.undo(ctx, \"doc:123\");\n\n// Or create a scoped facade\nconst doc = timeline.forScope(\"doc:123\");\nawait doc.push(ctx, { text: \"Hello\" });\nawait doc.undo(ctx);\n```\n\nSee\n[example/convex](https://github.com/MeshanKhosla/convex-timeline/tree/main/example/convex)\nfor a full example.\n\nhttps://github.com/user-attachments/assets/75f3a2da-f2b4-43c2-aade-aa537afb5bb3\n\n[Hosted version](https://todo-timeline.vercel.app/)\n\n## Constructor Options\n\n```ts\n// Unlimited history (default)\nnew Timeline(components.timeline);\n\n// Global limit\nnew Timeline(components.timeline, { maxNodesPerScope: 100 });\n\n// Per-prefix limits (longest match wins)\nnew Timeline(components.timeline, {\n  maxNodesPerScope: { \"doc:\": 200, \"scratch:\": 50 },\n});\n```\n\n## API\n\nAll methods are available both on `Timeline` (with scope parameter) and on the\nscoped facade from `forScope()`.\n\n### Timeline Operations\n\n| Method                        | Description                                               |\n| ----------------------------- | --------------------------------------------------------- |\n| `push(ctx, scope, state)`     | Push new state. Prunes nodes ahead of head if after undo. |\n| `undo(ctx, scope, count?)`    | Move head back. Returns state or `null` if at start.      |\n| `redo(ctx, scope, count?)`    | Move head forward. Returns state or `null` if at end.     |\n| `currentDocument(ctx, scope)` | Get current state without modifying timeline.             |\n| `status(ctx, scope)`          | Returns `{ canUndo, canRedo, position, length }`.         |\n| `clear(ctx, scope)`           | Remove all nodes, reset head. Preserves checkpoints.      |\n| `deleteScope(ctx, scope)`     | Delete scope and all data including checkpoints.          |\n\n### Checkpoints\n\nCheckpoints are named snapshots stored independently—they persist even when\nnodes are pruned.\n\n| Method                                    | Description                                    |\n| ----------------------------------------- | ---------------------------------------------- |\n| `createCheckpoint(ctx, scope, name)`      | Save current state as named checkpoint.        |\n| `restoreCheckpoint(ctx, scope, name)`     | Push checkpoint state as new node (undoable).  |\n| `getCheckpointDocument(ctx, scope, name)` | Get checkpoint state without restoring.        |\n| `listCheckpoints(ctx, scope)`             | List all checkpoints with names and positions. |\n| `deleteCheckpoint(ctx, scope, name)`      | Delete a checkpoint.                           |\n\n### Inspection\n\n| Method                                   | Description                                         |\n| ---------------------------------------- | --------------------------------------------------- |\n| `getDocumentAtPosition(ctx, scope, pos)` | Get state at specific position without moving head. |\n| `listNodes(ctx, scope)`                  | List all nodes with positions and documents.        |\n\n## How It Works\n\n```\nAfter three pushes:\n    [A] → [B] → [C]\n                head\n\nAfter undo:\n    [A] → [B] → [C]\n          head\n\nAfter push(D) — C is pruned:\n    [A] → [B] → [D]\n                head\n```\n\nThis pruning behavior matches the standard undo/redo model used by editors like\nGoogle Docs, VSCode, and Notion, where pushing new state after an undo discards\nthe forward history. Future versions may support alternative models such as\nmanual branching, time travel, and reconciliation strategies, depending on\ncommunity interest.\n\nCheckpoints persist through pruning:\n\n```\ncreateCheckpoint(\"v1\") at C → saves C's state\nAfter C is pruned → checkpoint \"v1\" still holds C\nrestoreCheckpoint(\"v1\") → pushes C as new node (undoable)\n```\n\n## Example\n\nSee [example/](./example) for a complete todo app with undo/redo.\n\n```sh\ncd example && npm install && npm run dev\n```\n\n## FAQ\n\n### Why do we prune the redo branch?\n\nI looked at many text editing programs (Google docs, VSCode, Notion, etc) and\nmimicked their behavior. When we `undo` and `push`, the redo branch can not be\naccessed again so we prune it to save on storage space.\n\n### Why are checkpoints stored in a separate table?\n\nConsider the case where we checkpoint a node that gets pruned, either because\nthe scope reaches capacity or it's part of the redo branch. In this case, we can\nno longer restore to that state if we stored the checkpoint as a property of the\nnode.\n\n### Why does restoring a checkpoint create a new node instead of traveling back to it?\n\nThe original node might not exist anymore if it was pruned. Even if it does\nexist, creating a new node makes the restore undoable, meaning you can go back\nto where you were before restoring. It also protects the restored state from\nbeing pruned if you undo and push new state later. There is a future where I\nintroduce a time-travel mode if there is demand for it.\n\n### Why not use the Convex transaction log?\n\nIdeally the Convex transaction log (described\n[here](https://stack.convex.dev/how-convex-works#the-transaction-log)) can be\nused so we can specify pointers to data (obviously this would be more\ncomplicated since we'd need to consider indexes) but unfortunately Convex does\nnot currently expose the transaction log.\n\nIn addition to this, it's not necessary the case that the timeline component is\nonly used for tracking documents in tables. It can be used for any data!\n\n## License\n\nMIT\n","readmeFilename":"README.md"}