local isRoblox = _VERSION == "Luau" local isLune = _VERSION:find("Lune") == 1 local deps = if isLune then require("../deps") else require(script.Parent.deps) local requires = deps(isRoblox, isLune) local util = require("./util.luau") local Signal = util.Signal type Signal = util.Signal local task = requires.task local result = require("./result") type Result = result.Result local Ok = result.Ok local Err = result.Err local option = require("./option") type Option = option.Option local None = option.None local Some = option.Some --[=[ @class Future A future represents an asynchronous computation. A future is a value that might not have finished computing yet. This kind of “asynchronous value” makes it possible for a thread to continue doing useful work while it waits for the value to become available. ### The [Future:poll] Method The core method of future, poll, attempts to resolve the future into a final value. This method does not block if the value is not ready. Instead, the current task is executed in the background, and its progress is reported when polled. When using a future, you generally won’t call poll directly, but instead [Future:await] the value. ```lua local net = require("@lune/net") local fut: Future> = Future.try(function(url) local resp = net.request({ url = url, method = "GET", }) assert(resp.ok) return resp.body end, { "https://jsonplaceholder.typicode.com/posts/1" }) local resp: Result = fut:await() print(net.jsonDecode(resp:unwrap())) ``` ]=] local Future = {} --[=[ @private @type Status "initialized" | "pending" | "cancelled" | "ready" @within Future Represents the status of a [Future]. ]=] export type Status = "initialized" | "pending" | "cancelled" | "ready" --[=[ @private @interface Future @within Future Represents the internal state of a [Future]. @field _thread thread -- The background coroutine spawned for execution @field _ret T -- The value returned once execution has halted @field _spawnEvt Signal<()> -- Event for internal communication among threads pre execution @field _retEvt Signal, Status> -- Event for internal communication among threads post execution @field _status Status -- The status of the Future ]=] export type Future = typeof(Future) & { _ret: T, _thread: thread, _spawnEvt: Signal<()>, _retEvt: Signal, Status>, _status: Status, } local function _constructor(fn: (Signal<()>, Signal) -> ()) return setmetatable( { _thread = coroutine.create(fn), _spawnEvt = Signal.new(), -- This is a hack to make luau realize that this object and -- Future are related _retEvt = Signal.new() :: Signal, Status>, _status = "initialized", } :: Future, { __index = Future, } ) end --[=[ @within Future Constructs a [Future] from a function to be run asynchronously. :::caution If a the provided function has the possibility to throw an error, instead of any other rusty-luau types like [Result] or [Option], use [Future:try] instead. ::: @param fn -- The function to be executed asynchronously @param args -- The arguments table to be passed to to the function @return Future -- The constructed future ]=] function Future.new(fn: (...any) -> T, args: { any }) return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal) spawnEvt:Fire() local ret = fn(table.unpack(args)) retEvt:Fire(ret, "ready") end) end --[=[ @within Future Constructs a fallible [Future] from a function to be run asynchronously. @param fn -- The fallible function to be executed asynchronously @param args -- The arguments table to be passed to to the function @return Future> -- The constructed future ]=] function Future.try(fn: (...any) -> T, args: { any }) return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal, Status>) spawnEvt:Fire() local ok, ret = pcall(fn, table.unpack(args)) local res: Result = if ok then Ok(ret) else Err(ret) retEvt:Fire(res, "ready") end) end --[=[ @within Future Polls a [Future] to completion. @param self Future @return (Status, Option) -- Returns the [Status] and an optional return if completed ]=] function Future.poll(self: Future): (Status, Option) if self._status == "initialized" then self._retEvt:Connect(function(firedRet, status: Status) self._status = status self._ret = firedRet -- Cleanup coroutine.yield(self._thread) coroutine.close(self._thread) self._spawnEvt:DisconnectAll() self._retEvt:DisconnectAll() end) self._spawnEvt:Connect(function() self._status = "pending" end) coroutine.resume(self._thread, self._spawnEvt, self._retEvt) end if self._status == "pending" then -- Just wait a bit more for the signal to fire task.wait(0.01) end local retOpt = if self._ret == nil then None() else Some(self._ret) return self._status, retOpt end --[=[ @within Future Cancels a [Future]. @param self Future ]=] function Future.cancel(self: Future) self._retEvt:Fire(nil :: any, "cancelled") self._status = "cancelled" end --[=[ @within Future Suspend execution until the result of a [Future] is ready. This method continuosly polls a [Future] until it reaches completion. @param self Future @return T -- The value returned by the function on completion ]=] function Future.await(self: Future): Option while true do local status: Status, ret: Option = self:poll() if status == "ready" then -- Safe to unwrap, we know it must not be nil return ret end end end return Future