{"id":"sqzee1/pipeline","name":"pipeline","scope":"sqzee1","platform":"roblox","description":"Pipeline for Luau, with control over phase order, sync/async execution, and graceful or abrupt cancellation.","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"45138c092056812087a33a56f133fb57a243f945fabd30f2a772559cb6c34e5c","likes":0,"downloads":0,"install":"forest install sqzee1/pipeline","url":"https://forest.dev/p/roblox/sqzee1/pipeline","files":"https://api.forest.dev/ai/package/roblox/sqzee1/pipeline/files","readme":"# 👾 Pipeline\n\nA ordered pipeline for Luau. Register callbacks as phases, control execution order, run them sync or async, and cancel gracefully or abruptly at any point.\n\n## Installation\n\nInstall via [Wally](https://wally.run) by adding Pipeline to your `wally.toml`:\n\n```toml\n[dependencies]\nPipeline = \"sqzee1/pipeline@1.0.0\"\n```\n\n---\n\n## Basic Usage\n\n```luau\nlocal Pipeline = require(path.to.Pipeline)\n\nlocal pipeline = Pipeline.new(function()\n    print(\"first\")\nend, function()\n    print(\"second\")\nend)\n\npipeline:run()\n```\n\n### Adding phases with hooks\n\n```luau\npipeline:addPhase(function()\n    print(\"does the work\")\nend, {\n    OnEnter = function()\n        print(\"about to start\")\n    end,\n    OnExit = function()\n        print(\"just finished\")\n    end,\n})\n```\n\n### Running async\n\n```luau\npipeline:run(true) -- doesn't block the calling thread\n```\n\n### Passing arguments\n\nAnything after `async` in `run` is forwarded to every phase's `OnEnter`, `Callback`, and `OnExit`:\n\n```luau\npipeline:run(false, player, 10)\n-- every OnEnter/Callback/OnExit in this run receives (player, 10)\n```\n\n### Global hooks\n\n```luau\npipeline:onComplete(function(...)\n    print(\"finished every phase\")\nend)\n\npipeline:onStopped(function(...)\n    print(\"halted early via stop()\")\nend)\n```\n\n`onComplete` fires when every phase finishes normally; `onStopped` fires instead if `stop()` was called during the run. Note that `onStopped` — not `onComplete` — fires even when `stop()` was called during the *last* phase, so nothing was actually skipped.\n\nNeither fires if a phase errors — see [Errors](#errors).\n\n### Stopping\n\n```luau\npipeline:stop() -- lets the current phase (and its OnExit) finish, then halts\npipeline:stop(true) -- cuts immediately, skips the rest of the current phase\n```\n\nAbruptness only escalates. Once a run has been stopped with `stop(true)`, a later `stop()` or `clear()` without `abrupt` will not downgrade it back to a graceful stop.\n\n### Clearing\n\n```luau\npipeline:clear() -- waits for the current phase to finish, then wipes all phases\npipeline:clear(true) -- cuts immediately, then wipes all phases\n```\n\n---\n\n## Methods\n\n| Method | Description |\n|---|---|\n| `Pipeline.new(...: Callback)` | Creates a pipeline. Any callbacks passed in are registered as phases, in order |\n| `pipeline:addPhase(callback, options?)` | Registers a phase after the last one. `options?` = `{ OnEnter?, OnExit? }` |\n| `pipeline:insertBefore(callback, target?, options?)` | Inserts a phase before `target`'s phase. Appends at the end if `target` isn't found or omitted |\n| `pipeline:insertAfter(callback, target?, options?)` | Inserts a phase after `target`'s phase. Appends at the end if `target` isn't found or omitted |\n| `pipeline:remove(target)` | Removes the phase registered with `target` |\n| `pipeline:getCurrentPhase()` | Returns the phase currently running, or `nil` |\n| `pipeline:isRunning()` | Returns whether the pipeline is currently executing |\n| `pipeline:shuffle()` | Shuffles the registered phases into random order |\n| `pipeline:run(async?, ...)` | Runs all phases in order, forwarding `...` to every `OnEnter`/`Callback`/`OnExit`. Blocks the caller unless `async` is `true` |\n| `pipeline:stop(abrupt?)` | Halts execution. Waits for the current phase to finish unless `abrupt` is `true`. No-op if no run is in progress |\n| `pipeline:clear(abrupt?)` | Stops (same `abrupt` rules as above) and removes every registered phase. Wipes immediately if no run is in progress |\n| `pipeline:onComplete(callback?)` | Sets the callback fired when a run finishes every phase without being stopped |\n| `pipeline:onStopped(callback?)` | Sets the callback fired when `stop()` halts a run before it finishes |\n\n`target` in `insertBefore`/`insertAfter`/`remove` is the callback function used when the phase was registered.  If the same callback was registered more than once, only the first matching phase is targeted.\n\nEditing the phase list from inside a running phase (`addPhase`, `remove`, `insertBefore`, `insertAfter`) is safe: the run iterates a snapshot taken when it started, so those edits take effect on the next run rather than shifting the phases still queued in this one.\n\n`getCurrentPhase()` returns the pipeline's own phase table, not a copy. Treat it as read-only; mutating it corrupts the pipeline.\n\nCalling `run()` on a pipeline that is already running warns and does nothing.\n\n`stop()` only affects a run that is already in progress; calling it beforehand does not arm a stop for the next `run()`.\n\n---\n\n## Types\n\n```luau\nlocal Pipeline = require(path.to.Pipeline)\n\nlocal p: Pipeline.Pipeline = Pipeline.new()\n```\n\nExported: `Pipeline`, `Phase`, `Options`, `Callback`.\n\n---\n\n## Options\n\n`addPhase`'s `options` table accepts:\n\n```luau\npipeline:addPhase(callback, {\n    OnEnter = function() end, -- runs right before the phase's callback\n    OnExit = function() end,  -- runs right after the phase's callback\n})\n```\n\nBoth fields are optional.\n\n---\n\n## Errors\n\nIf any `OnEnter`, `Callback`, or `OnExit` raises, the run stops there: no later phase runs, and neither `onComplete` nor `onStopped` fires.\n\nThe pipeline always resets its internal state first, so it is never left stuck as \"running\" and can be run again:\n\n```luau\npipeline:addPhase(function()\n    error(\"boom\")\nend)\n\nlocal ok, err = pcall(function()\n    pipeline:run()\nend)\n\nprint(ok, err)              --> false, \"ServerScriptService.Server:2: boom\"\nprint(pipeline:isRunning()) --> false\n```\n\nThe error is re-raised with level `0`, so the message you get back is exactly the one the phase raised — position info comes from the original `error()` call, not from inside Pipeline.\n\nA pending `clear()` still applies before the error is re-raised.\n\nWhere the error surfaces depends on how the run was started:\n\n- **Sync** (`pipeline:run()`) — re-raised out of `run()`, so `pcall` around the call catches it, as above.\n- **Async** (`pipeline:run(true)`) — `run()` has already returned by then, so there is no caller to catch it. The error surfaces as an unhandled error in the spawned thread. Wrap the phase body yourself if an async run must not fail loudly.\n\n---\n\n*Made by sqzee1*\n","readmeTruncated":false}