{"id":"biotoxin495/taskscheduler","name":"taskscheduler","scope":"biotoxin495","platform":"roblox","description":"A small, dependency-free Roblox module for running one-shot callbacks at future Unix timestamps or after relative delays.","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"a7c756aa12a24527d32598b091301786776a97963b52f659f1ad36d86fa9d520","likes":0,"downloads":0,"install":"forest install biotoxin495/taskscheduler","url":"https://forest.dev/p/roblox/biotoxin495/taskscheduler","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/taskscheduler/files","readme":"# TaskScheduler — A lightweight scheduler for delayed and timestamped tasks\n\n**TaskScheduler** is a small, dependency-free Roblox module for running one-shot callbacks at future Unix timestamps or after relative delays.\n\nIt gives server and client systems centralized ownership of delayed work: individual cancellation, target-based cleanup, task inspection, and reliable overdue execution — all without a framework, kernel, Signal, Promise, or package dependency.\n\n## Quick example\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal TaskScheduler = require(ReplicatedStorage.TaskScheduler)\n\nlocal scheduler = TaskScheduler.new()\nscheduler:Start()\n\nscheduler:ScheduleAfter(5, function()\n    print(\"Five seconds have passed\")\nend)\n\nscheduler:ScheduleAt(os.time() + 60, function()\n    print(\"The requested Unix time has been reached\")\nend)\n```\n\n`Start()` is idempotent. Calling it while the scheduler is already running returns `false` and does not create another loop.\n\n## 🚀 Features\n\n* Schedule tasks at absolute Unix timestamps with `ScheduleAt`\n* Schedule tasks after relative delays with `ScheduleAfter`, including fractional seconds\n* Execute overdue tasks instead of silently skipping them\n* Cancel individual tasks through returned handles\n* Group tasks under any target and cancel the whole group at once\n* Start, stop, manually step, clear, and destroy scheduler instances\n* Prevent duplicate processing loops\n* Run callbacks asynchronously so one task cannot block the scheduler\n* Catch callback errors with tracebacks\n* Optional centralized error handler\n* Optional task names and metadata\n* Compatibility helpers for the original `AddTask` API\n* Strict Luau types\n\n## 📖 Basic usage\n\nCopy `src/TaskScheduler.lua` into your project (or import `TaskScheduler.rbxmx`) and require it from any script that needs delayed work — it has no client/server restriction.\n\n### Creating a scheduler\n\nPass a configuration table to `TaskScheduler.new` to set its options.\n\n```lua\nlocal scheduler = TaskScheduler.new({\n    PollInterval = 0.1,\n    AutoStart = true,\n    OnError = function(handle, errorMessage)\n        warn(handle:GetName(), errorMessage)\n    end,\n})\n```\n\nA smaller `PollInterval` improves dispatch precision but wakes the scheduler more frequently.\n\n### Scheduling absolute tasks\n\n`ScheduleAt` uses `os.time()`. It is suitable for saved timestamps, shared event deadlines, daily resets, offer expiration, and other wall-clock operations.\n\n```lua\nscheduler:ScheduleAt(expirationTimestamp, expireOffer, {\n    Target = offer,\n    Name = \"ExpireOffer\",\n    Metadata = { OfferId = offer.Id },\n})\n```\n\nIf the timestamp has already passed, the task runs during the next scheduler step — it is not discarded.\n\n### Scheduling relative tasks\n\n`ScheduleAfter` uses Roblox's runtime `time()` clock and supports fractional delays. Delays must be zero or greater.\n\n```lua\nscheduler:ScheduleAfter(0.5, function()\n    print(\"Half a second later\")\nend)\n```\n\n### Target-based cleanup\n\nTasks can be grouped by an arbitrary target — a Player, Instance, table, string, or any other non-nil value:\n\n```lua\nlocal handle = scheduler:ScheduleAfter(60, callback, {\n    Target = player,\n})\n\nlocal cancelledCount = scheduler:CancelTarget(player)\n```\n\nThis is useful for player sessions, temporary UI objects, rounds, matches, offers, NPCs, and other objects with a shared lifecycle.\n\nTaskScheduler does not automatically listen for `PlayerRemoving` or Instance destruction, so connect cleanup explicitly to keep the core module general-purpose:\n\n```lua\nPlayers.PlayerRemoving:Connect(function(player)\n    scheduler:CancelTarget(player)\nend)\n```\n\n## ⚙️ API\n\n### Scheduler\n\n#### `TaskScheduler.new(options?)`\n\nCreates a new, isolated scheduler instance.\n\n```lua\nlocal scheduler = TaskScheduler.new()\n```\n\n#### `scheduler:Start()`\n\nBegins automatic processing. Returns `false` if already running.\n\n#### `scheduler:Stop()`\n\nStops automatic processing but preserves pending tasks. Overdue tasks are processed normally once the scheduler restarts, and running callbacks are not interrupted.\n\n#### `scheduler:IsRunning()`\n\nReturns whether automatic processing is currently active.\n\n#### `scheduler:Step()`\n\nPerforms one due-task check and dispatches every currently due task. Useful for controlled systems and tests — each callback still runs in its own spawned thread.\n\n```lua\nlocal dispatchedCount = scheduler:Step()\n```\n\n#### `scheduler:ScheduleAt(unixTime, callback, options?)`\n\nSchedules a callback against Unix time and returns a `TaskHandle`.\n\n#### `scheduler:ScheduleAfter(delaySeconds, callback, options?)`\n\nSchedules a callback after a runtime delay and returns a `TaskHandle`.\n\n#### `scheduler:Cancel(handleOrId)`\n\nCancels one pending task by handle or numeric ID. Returns `true` only when the task was still pending and was successfully cancelled.\n\n```lua\nscheduler:Cancel(handle)\nscheduler:Cancel(handle:GetId())\n```\n\n#### `scheduler:CancelTarget(target)`\n\nCancels every pending task associated with `target` and returns the number cancelled.\n\n#### `scheduler:CancelAt(unixTime, target?)`\n\nCancels absolute tasks (created with `ScheduleAt`) at an exact timestamp, optionally restricted to one target.\n\n#### `scheduler:GetPendingCount(target?)`\n\nReturns the number of pending tasks, or the number pending under a target.\n\n#### `scheduler:Clear()`\n\nCancels every pending task. Running callbacks are unaffected.\n\n#### `scheduler:Destroy()`\n\nStops the processing loop, cancels all pending tasks, and makes the scheduler unusable. Repeated calls are safe.\n\n### TaskHandle\n\nEvery scheduled task returns a handle:\n\n```lua\nlocal handle = scheduler:ScheduleAfter(20, callback)\n```\n\n#### `handle:Cancel()`\n\nCancels the task if it is still pending.\n\n```lua\nlocal didCancel = handle:Cancel()\n```\n\nReturns `true` only on success — a task cannot be cancelled after it begins running.\n\n#### `handle:IsPending()`\n\nReturns whether cancellation is still possible.\n\n#### `handle:GetId()`\n\nReturns the scheduler-local task ID.\n\n#### `handle:GetStatus()`\n\nReturns the current lifecycle status: `Pending`, `Running`, `Completed`, `Cancelled`, or `Failed`.\n\n#### `handle:GetScheduledTime()`\n\nReturns the Unix or runtime due value.\n\n#### `handle:GetClockKind()`\n\nReturns `\"Unix\"` or `\"Runtime\"`, identifying the clock used by the task.\n\n#### `handle:GetTarget()`\n\nReturns the organizational target, or `nil`.\n\n#### `handle:GetName()`\n\nReturns the optional debug name, or `nil`.\n\n#### `handle:GetMetadata()`\n\nReturns optional caller-provided metadata, or `nil`.\n\n## Complete configuration reference\n\n### Constructor options\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `PollInterval` | `number` | `0.25` | Delay between automatic due-task checks. Must be greater than zero. |\n| `AutoStart` | `boolean` | `false` | Starts the scheduler during construction. |\n| `OnError` | `(handle, errorMessage) -> ()` | `nil` | Receives callback failures and their tracebacks. |\n\n### Schedule options\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `Target` | `any` | Organizational key used for grouping and cleanup. Not inspected or owned by the scheduler. |\n| `Name` | `string` | Optional debug name, surfaced through `handle:GetName()`. |\n| `Metadata` | `any` | Retained by the handle and not interpreted by the scheduler. |\n\n## Error handling\n\nCallbacks are executed through `xpcall` and receive a traceback when they fail.\n\nWithout a custom error handler, failures are sent to `warn`:\n\n```lua\nlocal scheduler = TaskScheduler.new()\n```\n\nWith centralized handling:\n\n```lua\nlocal scheduler = TaskScheduler.new({\n    OnError = function(handle, errorMessage)\n        warn((\"Task %d failed: %s\"):format(handle:GetId(), errorMessage))\n    end,\n})\n```\n\nA failed callback receives the `Failed` status. One task's failure does not stop the scheduler or other callbacks.\n\n## Original API compatibility\n\nThe release includes compatibility helpers for the original module:\n\n```lua\nscheduler:AddTask(target, unixTime, callback)\nscheduler:RemoveTask(target, unixTime)\nscheduler:RemoveTasksByTarget(target)\n```\n\nThey map to:\n\n```lua\nscheduler:ScheduleAt(unixTime, callback, { Target = target })\nscheduler:CancelAt(unixTime, target)\nscheduler:CancelTarget(target)\n```\n\n`AddTask` now returns a task handle. `RemoveTask` still removes all matching tasks for the target and timestamp, while `handle:Cancel()` allows individual cancellation.\n\nThe old `PlayerAdded` initialization is no longer necessary because target indexes are created lazily. Player removal should call `CancelTarget(player)` directly.\n\n## 📝 Notes\n\n* Actual dispatch timing depends on `PollInterval`, Roblox task scheduling, frame time, server load, and callback thread scheduling — TaskScheduler does not guarantee execution on the exact frame or millisecond requested, only that a pending task is dispatched once its scheduled time is reached or passed while the scheduler is processing.\n* Tasks that become overdue while the scheduler is stopped are dispatched after it restarts.\n* Tasks due in the same scheduler step are ordered by scheduled time when they use the same clock, then by creation ID. Callbacks are spawned independently, so callback completion order is not guaranteed.\n* TaskScheduler is an in-memory runtime scheduler. Scheduled callbacks do not survive server shutdown, server crashes, teleports, or a new server session.\n* Lua callbacks cannot be serialized. For persistent behavior, save domain data such as an action type, object ID, and Unix timestamp, then reconstruct the scheduled callback after loading the data in a new server.\n* TaskScheduler is not a distributed or cross-server job system — separate servers maintain separate scheduler instances.\n* The scheduler scans its pending tasks once per processing step. This keeps the implementation small, inspectable, and reliable for ordinary gameplay systems with modest task counts. For very large queues containing thousands of long-lived tasks, a priority queue or dedicated persistent job architecture may be more appropriate.\n\n## 🛠️ Installation\n\n### Roblox Studio\n\nImport `TaskScheduler.rbxmx`, then place the `TaskScheduler` ModuleScript somewhere accessible to the scripts that use it, such as `ReplicatedStorage` or `ServerScriptService`.\n\n### Rojo\n\nCopy `src/TaskScheduler.lua` into your project, or use the included `default.project.json`:\n\n```bash\nrojo serve default.project.json\n```\n\nThe included project maps the module to `ReplicatedStorage.TaskScheduler`, examples to `ServerScriptService.TaskSchedulerExamples`, and the manual specification to `ReplicatedStorage.TaskSchedulerTests`.\n\n```text\nTaskScheduler/\n├── default.project.json\n├── README.md\n├── TaskScheduler.rbxmx\n├── examples/\n│   ├── Basic.server.lua\n│   └── PlayerCleanup.server.lua\n├── src/\n│   └── TaskScheduler.lua\n└── tests/\n    └── TaskScheduler.spec.lua\n```\n\nmade with ❤️ by biotoxin495\n","readmeTruncated":false}