{"id":"iamthebestts/chrono","name":"chrono","scope":"iamthebestts","platform":"roblox","description":"Unified time scheduler for Luau — tasks, intervals, scopes, profiling","version":"0.1.1","latest":"0.1.1","versions":["0.1.0","0.1.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"adcb2d2681cfe9d7c0c1ec7efde9eaf7f6f5ce12dfbf20426230af3b89fe95ef","likes":0,"downloads":0,"install":"forest install iamthebestts/chrono","url":"https://forest.dev/p/roblox/iamthebestts/chrono","files":"https://api.forest.dev/ai/package/roblox/iamthebestts/chrono/files","readme":"<div align=\"center\">\n\n# chrono\n\n**Unified time scheduler for Luau on Roblox.**\n\nOne API for delays, intervals, fixed-rate ticks, and per-frame callbacks — with scoped lifecycle, pause, time-scale, and a built-in profiler.\n\n![version](https://img.shields.io/badge/version-0.1.0-blue)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n[![CI](https://github.com/iamthebestts/chrono/actions/workflows/ci.yml/badge.svg)](https://github.com/iamthebestts/chrono/actions/workflows/ci.yml)\n\n<br/>\n\n[<img src=\".github/assets/link-wally.svg\" height=\"32\" alt=\"Install with Wally\" />](https://wally.run/package/iamthebestts/chrono)\n&nbsp;&nbsp;\n[<img src=\".github/assets/link-creator-store.svg\" height=\"32\" alt=\"Get on Creator Store\" />](https://create.roblox.com/store)\n\n<br/>\n\n[Documentation](https://iamthebestts.github.io/chrono) ·\n[API Reference](https://iamthebestts.github.io/chrono/api/Chrono) ·\n[Changelog](https://iamthebestts.github.io/chrono/changelog)\n\n</div>\n\n---\n\n## Why\n\nRoblox gives you `task.delay`, `task.wait`, and `RunService.Heartbeat`. They work, but every non-trivial codebase ends up rebuilding the same things on top: cancellation tokens, scope-based cleanup, pause/resume, time-scaling for slow motion, profiling hot callbacks. **chrono** is that layer, written once.\n\n```lua\nlocal chrono = require(path.to.chrono)\n\nlocal scope = chrono.scope()\n\nscope:every(5, function()\n    print(\"every 5 seconds\")\nend)\n\nscope:after(2, function()\n    print(\"once, in 2 seconds\")\nend)\n\nscope:frame(function(dt)\n    camera.CFrame *= CFrame.Angles(0, dt, 0)\nend)\n\n-- later: one call cleans up everything\nscope:destroy()\n```\n\n---\n\n## Features\n\n| Feature | Description |\n|---|---|\n| **Scoped lifecycle** | All tasks belong to a scope. Destroy the scope and every task inside is cancelled instantly. |\n| **`after` / `every`** | One-shot delays and repeating intervals. `every` fires immediately by default (`fireImmediately = false` to skip). |\n| **`frame`** | Per-frame callbacks via `RunService.Heartbeat` with wall-clock `dt`. |\n| **`tick`** | Fixed-rate simulation loop (e.g. 60 Hz physics) — fires multiple times per frame to catch up. |\n| **Pause & resume** | Scope-level and handle-level pause. `frame` still fires with `dt = 0` while scope is paused. |\n| **Time-scale** | `scope:setTimeScale(0.5)` for slow motion, `2` for fast-forward. Affects `after`, `every`, `tick` — not `frame`. |\n| **maxCatchup** | Cap how many times `tick` fires per frame after a hitch: `scope:tick(60, fn, { maxCatchup = 4 })`. |\n| **Error isolation** | A crashing callback never kills other tasks. Optional `onError` handler per scope. |\n| **Profiler** | `chrono.profile()` returns per-task count, avg/max/p99 execution time. Ring-buffer backed. |\n| **Global controls** | `chrono.pause_all()` / `chrono.resume_all()` — freeze every scope in one call (e.g. pause menu). |\n| **Trove compatible** | Scopes expose `Destroy()` so Trove can clean them up automatically. |\n\n---\n\n## Installation\n\n### Wally\n\nAdd to your `wally.toml`:\n\n```toml\n[dependencies]\nchrono = \"iamthebestts/chrono@0.1.0\"\n```\n\nThen run `wally install`.\n\n### pesde\n\nAdd to your `pesde.toml`:\n\n```toml\n[dependencies]\nchrono = { name = \"iamthebestts/chrono\", version = \"^0.1.0\" }\n```\n\nThen run `pesde install`.\n\n### Roblox model\n\nGrab the `.rbxm` from [Releases](https://github.com/iamthebestts/chrono/releases) or the [Creator Store](https://create.roblox.com/store) and drop it into `ReplicatedStorage`.\n\n---\n\n## Quick start\n\n```lua\nlocal chrono = require(ReplicatedStorage.Packages.chrono)\n\n-- 1. Create a scope\nlocal scope = chrono.scope()\n\n-- 2. Run something once after a delay\nscope:after(3, function()\n    print(\"3 seconds later!\")\nend)\n\n-- 3. Run something every N seconds (fires immediately, then repeats)\nlocal handle = scope:every(5, function()\n    print(\"heartbeat every 5s\")\nend)\n\n-- 4. Run every frame\nscope:frame(function(dt)\n    part.Position += Vector3.new(0, speed * dt, 0)\nend)\n\n-- 5. Fixed-rate physics at 60 Hz\nscope:tick(60, function(dt)\n    player.Position += player.Velocity * dt\nend)\n\n-- 6. Pause / resume\nscope:pause()   -- freezes all timers; frame fires with dt=0\nscope:resume()  -- picks up where it left off, no backlog\n\n-- 7. Slow motion\nscope:setTimeScale(0.5)\n\n-- 8. Cancel a single task\nhandle:cancel()\n\n-- 9. Destroy the scope — cancels everything\nscope:destroy()\n```\n\n---\n\n## API\n\n> Full documentation at **[iamthebestts.github.io/chrono](https://iamthebestts.github.io/chrono)**\n\n### Module\n\n| Method | Description |\n|---|---|\n| `chrono.scope(config?)` | Create a new scope. Config: `{ timeScale: number?, onError: ((err, name) -> ())? }` |\n| `chrono.profile()` | Returns profiling data for all named tasks. |\n| `chrono.reset_profiler()` | Clears all profiler data. |\n| `chrono.pause_all()` | Pauses every active scope. |\n| `chrono.resume_all()` | Resumes every active scope. |\n\n### Scope\n\n| Method | Description |\n|---|---|\n| `scope:after(delay, fn, config?)` | Schedule `fn` once after `delay` seconds. Returns a `Handle`. |\n| `scope:every(interval, fn, config?)` | Schedule `fn` every `interval` seconds. Fires immediately by default (`fireImmediately = false` to skip). Returns a `Handle`. |\n| `scope:frame(fn, config?)` | Schedule `fn` every frame with `dt`. Returns a `Handle`. |\n| `scope:tick(hz, fn, config?)` | Schedule `fn` at fixed `hz` rate. Config accepts `maxCatchup`. Returns a `Handle`. |\n| `scope:pause()` | Freeze all tasks. `frame` still fires with `dt = 0`. |\n| `scope:resume()` | Unfreeze all tasks. No backlog. |\n| `scope:setTimeScale(scale)` | Multiply time for `after`/`every`/`tick`. Negative values are clamped to `0`. Does **not** affect `frame`. |\n| `scope:destroy()` | Permanently cancel all tasks. Trove-compatible via `Destroy()`. |\n\n| Property | Type | Description |\n|---|---|---|\n| `scope.timeScale` | `number` | Current time multiplier (read-only). |\n| `scope.isPaused` | `boolean` | Whether the scope is paused (read-only). |\n| `scope.isDestroyed` | `boolean` | Whether the scope has been destroyed (read-only). |\n| `scope.elapsedTime` | `number` | Accumulated scaled time (read-only). |\n\n### Handle\n\n| Method | Description |\n|---|---|\n| `handle:cancel()` | Remove task from schedule. Handle becomes invalid. |\n| `handle:pause()` | Freeze this task individually. Cumulative with scope pause. |\n| `handle:resume()` | Unfreeze this task. |\n| `handle:setInterval(interval)` | Change interval (`every`/`tick` only). Resets accumulator. |\n| `handle:isPaused()` | Returns handle-level pause state (not scope). |\n| `handle:isCancelled()` | Returns whether the handle has been cancelled. |\n\n### Config\n\nAll scheduling methods accept an optional config table:\n\n```lua\nscope:after(1, fn, { name = \"respawn_timer\" })\nscope:tick(60, fn, { name = \"physics\", maxCatchup = 4 })\n```\n\n| Field | Applies to | Description |\n|---|---|---|\n| `name` | all | Label for the profiler. Default: `\"<anonymous>\"`. |\n| `maxCatchup` | `tick` | Max fires per frame during catch-up. Default: unlimited. |\n| `fireImmediately` | `every` | Fire once on registration before the first interval. Default: `true`. |\n\n---\n\n## Profiler\n\n```lua\nlocal data = chrono.profile()\n\nfor name, entry in data do\n    print(name, \"avg:\", entry.avgMs, \"max:\", entry.maxMs, \"p99:\", entry.p99Ms)\nend\n\n-- entry shape:\n-- {\n--     name: string,\n--     count: number,\n--     avgMs: number,\n--     maxMs: number,\n--     p99Ms: number,\n--     totalMs: number,\n-- }\n\nchrono.reset_profiler() -- clear all data\n```\n\n---\n\n## Integration\n\n### With Trove\n\n```lua\nlocal trove = Trove.new()\nlocal scope = chrono.scope()\n\ntrove:Add(scope) -- Trove calls scope:Destroy() on cleanup\n```\n\n### With Signals\n\n```lua\nlocal scope = chrono.scope()\n\nscope:every(5, function()\n    mySignal:Fire(\"tick\")\nend)\n```\n\n### Alongside RunService\n\nchrono uses a single `RunService.Heartbeat` connection internally. Your own connections work independently — no conflicts.\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for setup, conventions, and workflow.\n\n---\n\n## License\n\nReleased under the [MIT License](LICENSE).\n","readmeTruncated":false}