Roblox Lua Promise

repository·master·Indexed 43 days ago

https://github.com/evaera/roblox-lua-promise

A Promise/A+ pattern implementation for Roblox Lua designed for predictable, composable, and cancellable asynchronous workflows. It provides an alternative to the default Roblox yielding model, offering standardized error handling, concurrency management, and tools to convert yielding or event-based APIs into Promises via methods like Promise.new, Promise.promisify, and Promise.fromEvent.

Tokens
5.6K
Snippets
17
Records
26
Agent score
56%

What's inside Roblox Lua Promise

  1. Overview of Roblox Lua Promise

    master
    Roblox Lua Promise is an implementation of the Promise pattern, similar to Promise/A+, designed for the Roblox environment. It provides a way to represent a unit of asynchronous work that is composable and has predictable timing, offering an alternative to the default Roblox yielding model.
  2. What is a Promise and how does it work?

    master

    A Promise is an abstraction over Lua coroutines that represents a value that will exist in the future. Instead of yielding the current thread (which can cause unintentional delays and race conditions), an asynchronous function returns a Promise object synchronously.

    Key behaviors:

    • Resolving: When the asynchronous operation succeeds, the Promise is 'resolved' with a value. You can attach a callback using .andThen() to handle this value.
    • Rejecting: If the operation fails, the Promise is 'rejected' with an error. You can attach a callback using .catch() to handle the error.
    • Non-blocking: Because the function returns the Promise object immediately, the calling thread does not stop execution while waiting for the background task to complete.
    local function httpGet(url)
    	return Promise.new(function(resolve, reject)
    		local ok, result = pcall(HttpService.GetAsync, HttpService, url)
    
    		if ok then
    			resolve(result)
    		else
    			reject(result)
    		end
    	end)
    end
    
    -- Usage:
    httpGet("https://google.com")
    	:andThen(function(body)
    		print("Success:", body)
    	end)
    	:catch(function(err)
    		warn("Failed:", err)
    	end)
  3. Cancel Promises and use onCancel hooks

    master

    Promises support cancellation. While cancelling a Promise prevents it from resolving or rejecting, it does not automatically stop the underlying code from running unless you implement a cancellation hook.

    Implementing onCancel

    To properly abort ongoing operations (like disconnecting events or stopping loops), use the third argument onCancel provided in the Promise.new executor.

    Cancellation Behavior

    • Immediate Cancellation: If you cancel a Promise immediately after creation, it will only prevent resolution if the executor yields. If the executor resolves without yielding, it is already settled and cannot be cancelled.
    • Chained Cancellation: If a Promise is cancelled, any .andThen() or .catch() handlers attached to it will also be cancelled.
    • Automatic Cancellation: Chaining can cause automatic cancellation if no consumers are left to care about the value.
    local function myFunction()
    	return Promise.new(function(resolve, reject, onCancel)
    		local connection
    		connection = someEvent:Connect(function(...)
    			connection:Disconnect()
    			resolve(...)
    		end)
    
    		-- This hook runs if the promise is cancelled
    		onCancel(function()
    			connection:Disconnect()
    		end)
    	end)
    end
  4. Why use Promises instead of yielding in Roblox

    master

    While Roblox's default model for asynchronous operations is yielding (stopping the thread until a value is available), using Promises helps avoid several common issues:

    • Unpredictable Yielding: Prevents bugs and race conditions caused by functions that yield unexpectedly or inconsistently.
    • Concurrency Management: Makes it easier to run multiple asynchronous operations concurrently and retrieve their values without complex manual machinery.
    • Standardized Error Handling: Replaces the need for repetitive patterns used to check for errors or success values returned by standard Lua functions.
    • Introspection and Cancellation: Provides the ability to inspect the state of an operation and cancel it if the result is no longer needed, which is difficult with standard yielding.
  5. Understand Promise cancellation propagation

    master

    Cancellation in this library behaves in two directions:

    1. Upwards Propagation: Cancelling a Promise in a chain (the one returned by the last method) will propagate cancellation up the entire chain to the original Promise. This ensures that if the final consumer no longer needs the result, the entire sequence of dependencies is halted. Note: If a Promise has multiple andThen handlers attached, it will not be cancelled unless all of those handlers are also cancelled.

    2. Downwards Propagation: If a Promise is cancelled, any subsequent Promises that depend on it in a chain are also cancelled, as they can no longer resolve or reject.

    A Promise can be in one of four states: Started, Resolved, Rejected, or Cancelled. You can check the current state using promise:getStatus().

  6. Chain Promises with andThen, catch, and finally

    master

    Promises are chainable. Calling .andThen(), .catch(), or .finally() returns a new Promise that resolves with the value returned from the handler.

    • Value Transformation: Return a value from .andThen() to pass it to the next link in the chain.
    • Promise Chaining: If you return a new Promise from a handler, the chain will wait for that Promise to settle before moving to the next step.
    • Branching: You can call .andThen() multiple times on a single Promise to create multiple execution branches from one source.
    • Resolving with a Promise: If you call resolve(anotherPromise), the chain will wait for anotherPromise to settle and resolve with its value.
    -- Chaining values
    somePromise:andThen(function(number)
      return number + 1
    end):andThen(print)
    
    -- Chaining another Promise
    Promise.new(function(resolve)
    	somethingThatYields()
    	resolve(1)
    end):andThen(function(x)
    	return Promise.new(function(resolve)
    		somethingThatYields()
    		resolve(x + 1)
    	end)
    end):andThen(print) --> 2
  7. Understand Cancellation Propagation

    master

    Cancellation propagates through the Promise chain in two directions:

    1. Downwards: If a Promise is cancelled, all its consumers (the Promises created by .andThen() or .catch()) are also cancelled.
    2. Upwards: When a Promise is cancelled, it notifies its consumers. If a Promise no longer has any consumers, it is also cancelled. This propagation stops when it reaches a Promise that is already settled (resolved or rejected).

    Note: Resolving a Promise with another Promise makes the resolving Promise a consumer of the chained Promise. Therefore, if the chained Promise is cancelled, the resolving Promise will also be cancelled.

  8. Getting Started with Promises

    master

    To begin using the roblox-lua-promise library, follow these primary resources:

    • Installation: Follow the Installation guide to integrate the library into your Roblox project.
    • API Reference: Consult the API Docs for a complete list of methods, properties, and types available on the Promise object.
    • Conceptual Learning: Read the Tour of Promises to understand the core mechanics and the Why use Promises at all? guide to understand the benefits of asynchronous programming in Luau.
    • Practical Application: Browse the Examples for real-world usage patterns and implementation recipes.
  9. Cancel a Promise and clean up resources with onCancel

    master

    When a value is no longer needed (e.g., a player closes a menu or an ability is interrupted), you can cancel a Promise. Cancelling a Promise prevents andThen and catch handlers from running.

    To stop ongoing work (like a Tween or a network request) when a Promise is cancelled, use the third parameter of the Promise executor, onCancel. This allows you to register a callback that runs specifically when the Promise is cancelled.

    If you do not register an onCancel callback, the Promise will stop its chain, but the underlying asynchronous task (like a Tween) will continue to run in the background.

    local function tween(obj, tweenInfo, props)
    	return Promise.new(function(resolve, reject, onCancel)
    		local tween = TweenService:Create(obj, tweenInfo, props)
    			
    		-- Register a callback to be called if the Promise is cancelled.
    		onCancel(function()
    			tween:Cancel()
    		end) 
    		
    		tween.Completed:Connect(resolve)
    		tween:Play()
    	end)
    end
    
    -- Usage
    local promise = tween(workspace.Part, TweenInfo.new(2), { Transparency = 0.5 }):andThen(function()
    	print("This is never printed.")
    end):catch(function()
    	print("This is never printed.")
    end):finally(function()
    	print("But this *is* printed!")
    end)
    
    wait(1)
    promise:cancel() -- Cancel the Promise, which cancels the tween.
  10. Chain multiple Promise-returning functions

    master

    You can chain multiple functions that return Promises using :andThen(). This allows you to execute a sequence of asynchronous operations where each step depends on the previous one. If any function in the chain rejects (fails), execution will immediately skip all subsequent :andThen() calls and jump to the nearest :catch() handler.

    	doSomething()
    		:andThen(doSomethingElse)
    		:andThen(doSomethingOtherThanThat)
    		:andThen(doSomethingAgain)
    		:catch(print)
  11. Use Promise.delay and Promise.try instead of spawn, wait, and delay

    master

    Avoid using Roblox's native spawn, wait, or delay for asynchronous logic, as they suffer from inconsistent resumption intervals and throttling. Use these Promise alternatives instead:

    • Promise.delay(seconds): Uses an accurate custom scheduler to execute code after a delay.
    • Promise.try(function): A safer alternative to spawn for quickly launching a new thread without blocking the current execution.
    • Promise:timeout(seconds): A convenience method that returns a rejected Promise if the original Promise does not resolve within the specified time.
    -- Accurate delay
    Promise.delay(5):andThen(function()
    	print("5 seconds have passed!")
    end)
    
    -- Non-blocking launch (spawn alternative)
    Promise.try(function()
    	somethingThatYields()
    end)
    
    -- Timeout protection
    returnsAPromise():timeout(5):andThen(function()
    	print("This returned in at most 5 seconds")
    end)