{"_id":"@arcmantle/chronicle","_rev":"6-74becd2a7a8e229420bcc75686c1fc48","name":"@arcmantle/chronicle","dist-tags":{"latest":"1.0.5"},"versions":{"0.0.4":{"name":"@arcmantle/chronicle","version":"0.0.4","author":{"name":"Kristoffer Roen-Lie"},"license":"Apache-2.0","_id":"@arcmantle/chronicle@0.0.4","maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"homepage":"https://github.com/arcmantle/chronicle#readme","bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"dist":{"shasum":"734ca4cbf163111e6f6d54868c60c68c6b116fa9","tarball":"https://registry.npmjs.org/@arcmantle/chronicle/-/chronicle-0.0.4.tgz","fileCount":113,"integrity":"sha512-2DAuAOAAB4YYPlmdFOqN/E2OfYeEWMdtXdmbXKp+TjahelU2zRcgv/IHSzLcdqXunElxmDpbOKYq5J2UXKJ3jg==","signatures":[{"sig":"MEQCIFvfAhOlj6DEW7TREEhGm+pjjD3t5vIflz/Gh6V6ZWm0AiAwbpPiAzSp4KDXz2YVs3loNgMgcmu2xJrHO/Jzvvoh3Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":244196},"main":"./dist/index.js","type":"module","_from":"file:arcmantle-chronicle-0.0.4.tgz","types":"./dist/index.d.ts","exports":{"./*":"./dist/*.js"},"scripts":{"test":"vitest run","bench":"vitest bench --run","build":"rimraf dist && tsc --project ./src/tsconfig.json","docs:dev":"vitepress dev docs","docs:build":"typedoc --options docs/typedoc.json && vitepress build docs","docs:preview":"vitepress preview docs","docs:typedoc":"typedoc --options docs/typedoc.json"},"_npmUser":{"name":"roenlie","email":"kristofferroenlie@gmail.com"},"_resolved":"/tmp/e2fdf068c8fbc7854c4f40f98d8396db/arcmantle-chronicle-0.0.4.tgz","_integrity":"sha512-2DAuAOAAB4YYPlmdFOqN/E2OfYeEWMdtXdmbXKp+TjahelU2zRcgv/IHSzLcdqXunElxmDpbOKYq5J2UXKJ3jg==","repository":{"url":"git+https://github.com/arcmantle/chronicle.git","type":"git"},"_npmVersion":"11.6.2","description":"A library for managing changes over time with undo/redo functionality","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"vue":"^3.5.26","rimraf":"^6.1.2","vitest":"^4.0.16","typedoc":"^0.28.15","vitepress":"^1.6.4","typescript":"^5.9.3","@types/node":"^25.0.3","@arcmantle/tsconfig":"^1.0.9","typedoc-plugin-markdown":"^4.9.0","vitepress-mermaid-renderer":"^1.1.7"},"_npmOperationalInternal":{"tmp":"tmp/chronicle_0.0.4_1767137667368_0.06187346050817566","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@arcmantle/chronicle","version":"1.0.1","author":{"name":"Kristoffer Roen-Lie"},"license":"Apache-2.0","_id":"@arcmantle/chronicle@1.0.1","maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"homepage":"https://github.com/arcmantle/chronicle#readme","bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"dist":{"shasum":"49eb26351c65e807f6742b7a51659688168ca634","tarball":"https://registry.npmjs.org/@arcmantle/chronicle/-/chronicle-1.0.1.tgz","fileCount":113,"integrity":"sha512-QSO0/FkVXpLSustT3rvZW7JMR1XjGV+5GiMqX4k3uRddooLNFqzr+9MRa52ua17C6wGO8kiMzNbfxb4heSSU7w==","signatures":[{"sig":"MEQCIBTOyqYTUPRlmESKcCfVSgXJwEX9dGuKNk+YOgWMV4sbAiB4mRXhQzWnRfWmVybXqEDGMgH+S0byaKZqyj5H1ody2A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":244205},"main":"./dist/index.js","type":"module","_from":"file:arcmantle-chronicle-1.0.1.tgz","types":"./dist/index.d.ts","exports":{"./*":"./dist/*.js"},"scripts":{"test":"vitest run","bench":"vitest bench --run","build":"rimraf dist && tsc --project ./src/tsconfig.json","docs:dev":"vitepress dev docs","docs:build":"typedoc --options docs/typedoc.json && vitepress build docs","docs:preview":"vitepress preview docs","docs:typedoc":"typedoc --options docs/typedoc.json"},"_npmUser":{"name":"roenlie","email":"kristofferroenlie@gmail.com"},"_resolved":"/tmp/bdb6df593d5cd3a893c48f8784049417/arcmantle-chronicle-1.0.1.tgz","_integrity":"sha512-QSO0/FkVXpLSustT3rvZW7JMR1XjGV+5GiMqX4k3uRddooLNFqzr+9MRa52ua17C6wGO8kiMzNbfxb4heSSU7w==","repository":{"url":"git+https://github.com/arcmantle/chronicle.git","type":"git"},"_npmVersion":"11.6.2","description":"A library for managing changes over time with undo/redo functionality","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"vue":"^3.5.26","rimraf":"^6.1.2","vitest":"^4.0.16","typedoc":"^0.28.15","vitepress":"^1.6.4","typescript":"^5.9.3","@types/node":"^25.0.3","@arcmantle/tsconfig":"^1.0.9","typedoc-plugin-markdown":"^4.9.0","vitepress-mermaid-renderer":"^1.1.7"},"_npmOperationalInternal":{"tmp":"tmp/chronicle_1.0.1_1767138132745_0.10589690013269326","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@arcmantle/chronicle","version":"1.0.2","author":{"name":"Kristoffer Roen-Lie"},"license":"Apache-2.0","_id":"@arcmantle/chronicle@1.0.2","maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"homepage":"https://github.com/arcmantle/chronicle#readme","bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"dist":{"shasum":"7515c54ec6b505be1d67c214114c3f39df937f63","tarball":"https://registry.npmjs.org/@arcmantle/chronicle/-/chronicle-1.0.2.tgz","fileCount":113,"integrity":"sha512-IoClfqcwDz+BF0KeQBXWnwm/hoO6uYb/7zqGzCPy294Gazwou+tZJBRyPF/o0vNnH7SnDVwweFy+l05quBK6tQ==","signatures":[{"sig":"MEUCIQDNJQAprCpsic9sp0kQOqMpvJmu0absZ+bRbpBCCor52gIgQxxannZTVsHUbWpe8krethT7oLdh1+I5zer/T11YAaQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":244205},"main":"./dist/index.js","type":"module","_from":"file:arcmantle-chronicle-1.0.2.tgz","types":"./dist/index.d.ts","exports":{"./*":"./dist/*.js"},"scripts":{"test":"vitest run","bench":"vitest bench --run","build":"rimraf dist && tsc --project ./src/tsconfig.json","docs:dev":"vitepress dev docs","docs:build":"typedoc --options docs/typedoc.json && vitepress build docs","docs:preview":"vitepress preview docs","docs:typedoc":"typedoc --options docs/typedoc.json"},"_npmUser":{"name":"roenlie","email":"kristofferroenlie@gmail.com"},"_resolved":"/tmp/20a9ecfdb2e2f138872e88c28da6ab5e/arcmantle-chronicle-1.0.2.tgz","_integrity":"sha512-IoClfqcwDz+BF0KeQBXWnwm/hoO6uYb/7zqGzCPy294Gazwou+tZJBRyPF/o0vNnH7SnDVwweFy+l05quBK6tQ==","repository":{"url":"git+https://github.com/arcmantle/chronicle.git","type":"git"},"_npmVersion":"11.6.2","description":"A library for managing changes over time with undo/redo functionality","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"vue":"^3.5.26","rimraf":"^6.1.2","vitest":"^4.0.16","typedoc":"^0.28.15","vitepress":"^1.6.4","typescript":"^5.9.3","@types/node":"^25.0.3","@arcmantle/tsconfig":"^1.0.9","typedoc-plugin-markdown":"^4.9.0","vitepress-mermaid-renderer":"^1.1.7"},"_npmOperationalInternal":{"tmp":"tmp/chronicle_1.0.2_1767138293189_0.8072273859056447","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@arcmantle/chronicle","version":"1.0.3","author":{"name":"Kristoffer Roen-Lie"},"license":"Apache-2.0","_id":"@arcmantle/chronicle@1.0.3","maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"homepage":"https://github.com/arcmantle/chronicle#readme","bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"dist":{"shasum":"8c2a7471b498a2aa09bdb440d9d0b69033ba6aec","tarball":"https://registry.npmjs.org/@arcmantle/chronicle/-/chronicle-1.0.3.tgz","fileCount":113,"integrity":"sha512-IHGQDyWRnxUktHx35aK8L6HRT/VsO/j4sKoWI9ienrVg0mMYvsz/QQbY/YHHUr4Mmx8ROmqIjUm/dfLKzE0a7A==","signatures":[{"sig":"MEUCIBwhhcbpL1ZAg6PculwfpuU9dqQmNRFQ4OjxzecsJKxvAiEAhagAArCoJ04FTECYSpKUH76PpDK14QI3M7XZCMTZ5tQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":247234},"main":"./dist/index.js","type":"module","_from":"file:arcmantle-chronicle-1.0.3.tgz","types":"./dist/index.d.ts","exports":{"./*":"./dist/*.js"},"scripts":{"test":"vitest run","bench":"vitest bench --run","build":"rimraf dist && tsc --project ./src/tsconfig.json","docs:dev":"vitepress dev docs","docs:build":"typedoc --options docs/typedoc.json && vitepress build docs","docs:preview":"vitepress preview docs","docs:typedoc":"typedoc --options docs/typedoc.json"},"_npmUser":{"name":"roenlie","email":"kristofferroenlie@gmail.com"},"_resolved":"/tmp/a3ae1d38c64151ab2a664c267e5ea2b1/arcmantle-chronicle-1.0.3.tgz","_integrity":"sha512-IHGQDyWRnxUktHx35aK8L6HRT/VsO/j4sKoWI9ienrVg0mMYvsz/QQbY/YHHUr4Mmx8ROmqIjUm/dfLKzE0a7A==","repository":{"url":"git+https://github.com/arcmantle/chronicle.git","type":"git"},"_npmVersion":"11.6.2","description":"A library for managing changes over time with undo/redo functionality","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"vue":"^3.5.26","rimraf":"^6.1.2","vitest":"^4.0.16","typedoc":"^0.28.15","vitepress":"^1.6.4","typescript":"^5.9.3","@types/node":"^25.0.3","@arcmantle/tsconfig":"^1.0.9","typedoc-plugin-markdown":"^4.9.0","vitepress-mermaid-renderer":"^1.1.7"},"_npmOperationalInternal":{"tmp":"tmp/chronicle_1.0.3_1767138859045_0.05032564171917464","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@arcmantle/chronicle","version":"1.0.4","author":{"name":"Kristoffer Roen-Lie"},"license":"Apache-2.0","_id":"@arcmantle/chronicle@1.0.4","maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"homepage":"https://github.com/arcmantle/chronicle#readme","bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"dist":{"shasum":"241a94c89bdd592b1cbb8795c28c46bd986ae30c","tarball":"https://registry.npmjs.org/@arcmantle/chronicle/-/chronicle-1.0.4.tgz","fileCount":118,"integrity":"sha512-zeO8QWmqI8yW4tEjBnE9hi+OfC5nisg55kfgj16FOPiOeze/djJ62miwM+ZJ9+ga37TMi3/j4oaHydxl9j3rwQ==","signatures":[{"sig":"MEQCIBc+MdeWjdXtuIl3Gcfak9rYRMxlEamyZUv/q7OZlVfEAiAwL2WJssKWPaW+6z0ynXyXmjIlrRGaxTipNhp6g4sqvw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":270854},"main":"./dist/index.js","type":"module","_from":"file:arcmantle-chronicle-1.0.4.tgz","types":"./dist/index.d.ts","exports":{"./*":"./dist/*.js"},"scripts":{"test":"vitest run","bench":"vitest bench --run","build":"rimraf dist && tsc --project ./src/tsconfig.json","docs:dev":"vitepress dev docs","docs:build":"typedoc --options docs/typedoc.json && vitepress build docs","docs:preview":"vitepress preview docs","docs:typedoc":"typedoc --options docs/typedoc.json"},"_npmUser":{"name":"roenlie","email":"kristofferroenlie@gmail.com"},"_resolved":"/tmp/7b667bf5fc9b41f2107c0b65714d4217/arcmantle-chronicle-1.0.4.tgz","_integrity":"sha512-zeO8QWmqI8yW4tEjBnE9hi+OfC5nisg55kfgj16FOPiOeze/djJ62miwM+ZJ9+ga37TMi3/j4oaHydxl9j3rwQ==","repository":{"url":"git+https://github.com/arcmantle/chronicle.git","type":"git"},"_npmVersion":"11.6.2","description":"A library for managing changes over time with undo/redo functionality","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"vue":"^3.5.26","rimraf":"^6.1.2","vitest":"^4.0.16","typedoc":"^0.28.15","vitepress":"^1.6.4","typescript":"^5.9.3","@types/node":"^25.0.3","@arcmantle/tsconfig":"^1.0.9","typedoc-plugin-markdown":"^4.9.0","vitepress-mermaid-renderer":"^1.1.7"},"_npmOperationalInternal":{"tmp":"tmp/chronicle_1.0.4_1767204487400_0.13414098398101304","host":"s3://npm-registry-packages-npm-production"}},"1.0.5":{"name":"@arcmantle/chronicle","version":"1.0.5","description":"A library for managing changes over time with undo/redo functionality","repository":{"type":"git","url":"git+https://github.com/arcmantle/chronicle.git"},"license":"Apache-2.0","author":{"name":"Kristoffer Roen-Lie"},"sideEffects":false,"type":"module","exports":{"./*":"./dist/*.js"},"main":"./dist/index.js","types":"./dist/index.d.ts","devDependencies":{"@arcmantle/tsconfig":"^1.0.9","@types/node":"^25.0.3","rimraf":"^6.1.2","typedoc":"^0.28.15","typedoc-plugin-markdown":"^4.9.0","typescript":"^5.9.3","vitepress":"^1.6.4","vitepress-mermaid-renderer":"^1.1.7","vitest":"^4.0.16","vue":"^3.5.26"},"scripts":{"bench":"vitest bench --run","build":"rimraf dist && tsc --project ./src/tsconfig.json","docs:build":"typedoc --options docs/typedoc.json && vitepress build docs","docs:dev":"vitepress dev docs","docs:preview":"vitepress preview docs","docs:typedoc":"typedoc --options docs/typedoc.json","test":"vitest run"},"_id":"@arcmantle/chronicle@1.0.5","bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"homepage":"https://github.com/arcmantle/chronicle#readme","_integrity":"sha512-DqUTFEvanIMFceV6V6ww68O0MjvN8RRwBW7DR5ihF42QFUPFxjxteNbWdJeNv5Qq1ba9GDI2GObA4NIzJphawg==","_resolved":"/tmp/c7975c590c057466d177d8ebef884495/arcmantle-chronicle-1.0.5.tgz","_from":"file:arcmantle-chronicle-1.0.5.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-DqUTFEvanIMFceV6V6ww68O0MjvN8RRwBW7DR5ihF42QFUPFxjxteNbWdJeNv5Qq1ba9GDI2GObA4NIzJphawg==","shasum":"40c353988ca1a186376c7955bf6a3adff2ac2a37","tarball":"https://registry.npmjs.org/@arcmantle/chronicle/-/chronicle-1.0.5.tgz","fileCount":118,"unpackedSize":288108,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDoxhh1LudkyTbPAI2CZFiCqcRqZI/2THFBLaiWjconDQIgXCSk5b+mqhc8OYoheyAx16Jt6bVYj4XZBALbN5joU/Y="}]},"_npmUser":{"name":"roenlie","email":"kristofferroenlie@gmail.com"},"directories":{},"maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/chronicle_1.0.5_1767279534056_0.9141045177114822"},"_hasShrinkwrap":false}},"time":{"created":"2025-12-30T23:34:27.301Z","modified":"2026-01-01T14:58:54.462Z","0.0.4":"2025-12-30T23:34:27.529Z","1.0.1":"2025-12-30T23:42:12.933Z","1.0.2":"2025-12-30T23:44:53.343Z","1.0.3":"2025-12-30T23:54:19.205Z","1.0.4":"2025-12-31T18:08:07.559Z","1.0.5":"2026-01-01T14:58:54.229Z"},"bugs":{"url":"https://github.com/arcmantle/chronicle/issues"},"author":{"name":"Kristoffer Roen-Lie"},"license":"Apache-2.0","homepage":"https://github.com/arcmantle/chronicle#readme","repository":{"type":"git","url":"git+https://github.com/arcmantle/chronicle.git"},"description":"A library for managing changes over time with undo/redo functionality","maintainers":[{"name":"roenlie","email":"kristofferroenlie@gmail.com"}],"readme":"# Chronicle - Deep Observable State with Time-Travel\n\nChronicle is a powerful state observation library that provides deep proxy-based tracking, history recording, undo/redo capabilities, and time-travel debugging for JavaScript objects.\n\n## Features\n\n- **Deep Observation**: Automatically tracks changes to nested objects, arrays, Maps, and Sets\n- **Time-Travel Debugging**: Full undo/redo with group-based operations\n- **Flexible Listeners**: Listen to specific paths with exact, descendant, or ancestor modes\n- **Batching & Transactions**: Group multiple changes into atomic, undoable operations\n- **Smart History**: Configurable history size, filtering, and compaction\n- **Diff & Snapshots**: Compare current state to original, reset to pristine\n- **Quality of Life**: Debounce, throttle, once listeners, pause/resume notifications\n\n## Quick Start\n\n```typescript\nimport { chronicle } from './chronicle.ts';\n\n// Observe an object\nconst state = chronicle({ count: 0, user: { name: 'Alice' } });\n\n// Listen to changes (string selector)\nchronicle.listen(state, 'count', (path, newValue, oldValue) => {\n  console.log(`Count changed from ${oldValue} to ${newValue}`);\n});\n\n// Or use a function selector for better type safety\nchronicle.listen(state, s => s.count, (path, newValue, oldValue) => {\n  console.log(`Count changed from ${oldValue} to ${newValue}`);\n});\n\n// Make changes\nstate.count = 1; // Listener fires: \"Count changed from 0 to 1\"\n\n// Undo\nchronicle.undo(state);\nconsole.log(state.count); // 0\n```\n\n## Core API\n\n### `chronicle(object)`\n\nWraps an object with deep observation. Returns a proxy that tracks all changes.\n\n```typescript\nconst observed = chronicle({ items: [], settings: { theme: 'dark' } });\n```\n\n### Listeners\n\n#### `chronicle.listen(object, selector, listener, mode?, options?)`\n\nListen to changes at a specific path.\n\n**Modes:**\n\n- `'exact'` (default): Only changes to this exact path\n- `'down'`: Changes to this path and all descendants\n- `'up'`: Changes to any ancestor of this path\n\n**Selector types:**\n\n- String: `'user.name'` or `'items.0'`\n- Array: `['user', 'name']` or `['items', 0]`\n- Function: `obj => obj.user.name` (uses `nameof` utility)\n\n**Options:**\n\n- `once: boolean` - Auto-unsubscribe after first call\n- `debounceMs: number` - Coalesce rapid changes\n- `throttleMs: number` - Limit call frequency\n- `schedule: 'sync' | 'microtask'` - When to deliver notifications\n\n```typescript\n// Listen to exact path (string selector)\nchronicle.listen(state, 'count', (path, newVal, oldVal, meta) => {\n  console.log('Count changed:', newVal);\n});\n\n// Or use a function selector for type safety\nchronicle.listen(state, s => s.count, (path, newVal, oldVal, meta) => {\n  console.log('Count changed:', newVal);\n});\n\n// Listen to all descendants\nchronicle.listen(state, 'user', (path) => {\n  console.log('User changed at:', path);\n}, 'down');\n\n// Function selector with descendant mode\nchronicle.listen(state, s => s.user, (path) => {\n  console.log('User changed at:', path);\n}, 'down');\n\n// Debounced listener\nchronicle.listen(state, s => s.searchQuery, handleSearch, {\n  debounceMs: 300\n});\n\n// Throttled listener\nchronicle.listen(state, s => s.mousePosition, updateUI, {\n  throttleMs: 16 // ~60fps\n});\n\n// One-time listener\nchronicle.listen(state, s => s.initialized, () => {\n  console.log('App initialized!');\n}, { once: true });\n```\n\n#### `chronicle.onAny(object, listener, options?)`\n\nListen to all changes on the object.\n\n```typescript\nchronicle.onAny(state, (path, newVal, oldVal, meta) => {\n  console.log('Changed:', path, 'type:', meta.type);\n});\n```\n\n### Pause/Resume\n\n```typescript\n// Pause notifications (queues them)\nchronicle.pause(state);\n\nstate.count = 1;\nstate.count = 2;\nstate.count = 3; // No listeners fired yet\n\n// Resume and deliver all queued notifications\nchronicle.resume(state);\n\n// Or just flush without resuming\nchronicle.flush(state);\n```\n\n### History\n\n```typescript\n// Get full history\nconst history = chronicle.getHistory(state);\n// [{ path: ['count'], type: 'set', oldValue: 0, newValue: 1, ... }]\n\n// Clear history\nchronicle.clearHistory(state);\n\n// Mark current point for undo\nconst marker = chronicle.mark(state);\n// ... make changes ...\nchronicle.undoSince(state, marker);\n```\n\n### Undo/Redo\n\n```typescript\n// Undo individual steps\nchronicle.undo(state, 3); // Undo last 3 changes\n\n// Undo by groups (batches/transactions)\nchronicle.undoGroups(state, 1); // Undo last batch\n\n// Redo\nchronicle.redo(state, 2);\nchronicle.redoGroups(state, 1);\n\n// Check availability\nif (chronicle.canUndo(state)) {\n  chronicle.undo(state);\n}\n\nif (chronicle.canRedo(state)) {\n  chronicle.redo(state);\n}\n\n// Clear redo stack\nchronicle.clearRedo(state);\n```\n\n### Batching\n\nGroup multiple changes into a single undoable operation.\n\n```typescript\n// Manual batching\nchronicle.beginBatch(state);\nstate.items.push('item1');\nstate.items.push('item2');\nstate.count = 2;\nchronicle.commitBatch(state);\n\n// Now undo reverts all 3 changes as one\nchronicle.undoGroups(state, 1);\n\n// Or rollback to discard changes\nchronicle.beginBatch(state);\nstate.count = 999;\nchronicle.rollbackBatch(state); // Changes discarded\n\n// Convenience wrapper\nchronicle.batch(state, (s) => {\n  s.items.push('item1');\n  s.items.push('item2');\n  s.count = 2;\n}); // Auto-commits\n\n// Batch with error handling\ntry {\n  chronicle.batch(state, (s) => {\n    s.count = 1;\n    throw new Error('Something went wrong');\n  });\n} catch (e) {\n  // Batch auto-rolled back on error\n}\n```\n\n### Transactions\n\nTransactions are batches with convenient undo helpers.\n\n```typescript\n// Sync transaction\nconst { result, marker, undo } = chronicle.transaction(state, (s) => {\n  s.user.name = 'Bob';\n  s.user.email = 'bob@example.com';\n  return s.user;\n});\n\n// Later, undo this specific transaction\nundo();\n\n// Async transaction\nconst { result, undo } = await chronicle.transactionAsync(state, async (s) => {\n  s.loading = true;\n  const data = await fetchData();\n  s.data = data;\n  s.loading = false;\n  return data;\n});\n\n// Nested transactions coalesce\nchronicle.transaction(state, (s) => {\n  s.count = 1;\n  chronicle.transaction(s, (s2) => {\n    s2.count = 2; // Both changes in one group\n  });\n});\n// Undo undoes both changes\n```\n\n### Diff & Reset\n\n```typescript\nconst original = { count: 0, items: ['a'] };\nconst state = chronicle(original);\n\nstate.count = 5;\nstate.items.push('b');\n\n// Get differences\nconst diff = chronicle.diff(state);\n// [\n//   { path: ['count'], kind: 'changed', oldValue: 0, newValue: 5 },\n//   { path: ['items', '1'], kind: 'added', newValue: 'b' }\n// ]\n\n// Check if pristine\nconsole.log(chronicle.isPristine(state)); // false\n\n// Reset to original\nchronicle.reset(state);\nconsole.log(state.count); // 0\nconsole.log(state.items); // ['a']\n\n// Mark new pristine point\nstate.count = 10;\nchronicle.markPristine(state);\nconsole.log(chronicle.isPristine(state)); // true\n```\n\n### Configuration\n\nChronicle provides sensible defaults out of the box, but you can customize behavior:\n\n```typescript\nchronicle.configure(state, {\n  // Merge ungrouped changes within time window (default: true)\n  // Groups rapid consecutive changes for better undo/redo UX\n  mergeUngrouped: true,\n  mergeWindowMs: 300, // default: 300ms\n\n  // Compact consecutive sets to same path (default: true)\n  // Reduces memory without losing information\n  compactConsecutiveSamePath: true,\n\n  // Limit history size (default: 1000)\n  // Trims by whole groups to prevent unbounded growth\n  maxHistory: 1000,\n\n  // Filter which changes to record\n  filter: (record) => !record.path.includes('_temp'),\n\n  // Enable proxy caching for stable identity (default: true)\n  cacheProxies: true,\n\n  // Custom clone function (default: structuredClone)\n  clone: (value) => JSON.parse(JSON.stringify(value)),\n\n  // Custom equality check (default: Object.is)\n  compare: (a, b) => a === b,\n\n  // Filter diff traversal\n  diffFilter: (path) => {\n    if (path[0] === '_internal') return false; // Skip\n    if (path[0] === 'large') return 'shallow'; // Don't recurse\n    return true; // Recurse normally\n  }\n});\n```\n\n**Default Configuration:**\n\n- `mergeUngrouped: true` - Groups rapid changes for intuitive undo/redo\n- `mergeWindowMs: 300` - 300ms window for grouping changes\n- `compactConsecutiveSamePath: true` - Optimizes memory for rapid updates\n- `maxHistory: 1000` - Prevents unbounded memory growth\n- `cacheProxies: true` - Stable proxy identity for better UI framework integration\n\n## Working with Collections\n\n### Arrays\n\nArrays work seamlessly with all features. Deleting by index uses splice to avoid holes.\n\n```typescript\nconst state = chronicle({ items: ['a', 'b', 'c'] });\n\nstate.items.push('d');\nstate.items[1] = 'B';\ndelete state.items[2]; // Uses splice internally\n\nchronicle.undo(state); // Restores 'c' at index 2\n```\n\n### Maps\n\n```typescript\nconst state = chronicle({ cache: new Map() });\n\nstate.cache.set('key1', 'value1');\nstate.cache.set('key2', 'value2');\nstate.cache.delete('key1');\nstate.cache.clear();\n\n// Listen to map changes\nchronicle.listen(state, 'cache', (path, newVal, oldVal, meta) => {\n  console.log('Map operation:', meta.type);\n  // meta contains: { collection: 'map', key: 'key1' }\n});\n\n// Undo works correctly\nchronicle.undoGroups(state, 1); // Undoes entire clear\n```\n\n### Sets\n\n```typescript\nconst state = chronicle({ tags: new Set() });\n\nstate.tags.add('javascript');\nstate.tags.add('typescript');\nstate.tags.delete('javascript');\n\nchronicle.undo(state); // Restores 'javascript'\n```\n\n## Common Patterns\n\n### Todo List with Undo\n\n```typescript\nconst todos = chronicle({\n  items: [],\n  filter: 'all'\n});\n\nfunction addTodo(text) {\n  chronicle.batch(todos, (state) => {\n    state.items.push({\n      id: Date.now(),\n      text,\n      completed: false\n    });\n  });\n}\n\nfunction toggleTodo(id) {\n  const todo = todos.items.find(t => t.id === id);\n  if (todo) todo.completed = !todo.completed;\n}\n\nfunction deleteTodo(id) {\n  const index = todos.items.findIndex(t => t.id === id);\n  if (index !== -1) todos.items.splice(index, 1);\n}\n\n// Undo last action\nchronicle.undoGroups(todos, 1);\n```\n\n### Form State with Validation\n\n```typescript\nconst form = chronicle({\n  values: { email: '', password: '' },\n  errors: {},\n  touched: {},\n  isValid: true\n});\n\n// Debounced validation\nchronicle.listen(form, 'values', (path) => {\n  validateForm();\n}, 'down', { debounceMs: 300 });\n\nfunction validateForm() {\n  const errors = {};\n  if (!form.values.email.includes('@')) {\n    errors.email = 'Invalid email';\n  }\n  form.errors = errors;\n  form.isValid = Object.keys(errors).length === 0;\n}\n\n// Transaction for submit\nasync function submitForm() {\n  const { result, undo } = await chronicle.transactionAsync(form, async (f) => {\n    f.submitting = true;\n    try {\n      const result = await api.post('/submit', f.values);\n      f.submitSuccess = true;\n      return result;\n    } catch (error) {\n      f.submitError = error.message;\n      throw error;\n    } finally {\n      f.submitting = false;\n    }\n  });\n  return result;\n}\n```\n\n### Collaborative Editor\n\n```typescript\nconst doc = chronicle({\n  content: '',\n  cursors: new Map(),\n  version: 0\n});\n\n// Batch local edits\nlet editBatch = null;\nfunction startEdit() {\n  if (!editBatch) {\n    chronicle.beginBatch(doc);\n    editBatch = setTimeout(() => {\n      chronicle.commitBatch(doc);\n      editBatch = null;\n    }, 1000);\n  }\n}\n\nfunction insert(pos, text) {\n  startEdit();\n  doc.content = doc.content.slice(0, pos) + text + doc.content.slice(pos);\n  doc.version++;\n}\n\n// Listen for remote changes\nchronicle.listen(doc, 'content', (path, newVal) => {\n  broadcastToRemote({ content: newVal, version: doc.version });\n}, { debounceMs: 100 });\n```\n\n## Performance Tips\n\n1. **Use batching** for bulk operations to reduce listener overhead\n2. **Proxy caching is enabled by default** for better performance\n3. **Use debounce/throttle** for high-frequency updates\n4. **Filter history** to exclude temporary/internal state\n5. **maxHistory is set to 1000 by default** to prevent unbounded growth\n6. **Use 'exact' mode** when possible (faster than 'down'/'up')\n7. **Rapid changes are auto-grouped** for intuitive undo/redo\n\n## Gotchas & Best Practices\n\n### Listener Path Modes\n\n```typescript\nconst state = chronicle({ user: { profile: { name: 'Alice' } } });\n\n// 'exact': Only fires when 'user' is reassigned\nchronicle.listen(state, 'user', handler, 'exact');\nstate.user = {}; // Fires\nstate.user.profile.name = 'Bob'; // Does NOT fire\n\n// 'down': Fires for user and all nested changes\nchronicle.listen(state, 'user', handler, 'down');\nstate.user = {}; // Fires\nstate.user.profile.name = 'Bob'; // Fires\n\n// 'up': Fires when any ancestor changes\nchronicle.listen(state, ['user', 'profile', 'name'], handler, 'up');\nstate.user.profile.name = 'Bob'; // Does NOT fire (not an ancestor)\nstate.user.profile = {}; // Fires (ancestor)\nstate.user = {}; // Fires (ancestor)\n```\n\n### Array Length Changes\n\nWhen shrinking arrays, deletes are synthesized for removed elements:\n\n```typescript\nconst state = chronicle({ items: [1, 2, 3, 4] });\nstate.items.length = 2; // Generates delete records for indices 2 and 3\n```\n\n### Redo is Cleared\n\nMaking any forward change clears the redo stack:\n\n```typescript\nchronicle.undo(state); // Can now redo\nstate.count = 5; // Clears redo stack\nchronicle.redo(state); // Does nothing\n```\n\n### Avoid Recording Internal Operations\n\n```typescript\n// Bad: Will record intermediate array operations\nstate.items.push(...largeArray);\n\n// Better: Use batch to group\nchronicle.batch(state, (s) => {\n  s.items.push(...largeArray);\n});\n\n// Best: Filter out internal paths\nchronicle.configure(state, {\n  filter: (rec) => !rec.path[0].startsWith('_')\n});\nstate._tempData = []; // Not recorded\n```\n\n## TypeScript Support\n\nChronicle is fully typed and preserves object types:\n\n```typescript\ninterface User {\n  name: string;\n  age: number;\n}\n\nconst user: User = chronicle({ name: 'Alice', age: 30 });\n// user is still typed as User, all properties autocomplete\n```\n\n## License\n\nApache-2\n\n..\n","readmeFilename":"README.md"}