2024-05-24 19:00:33 +01:00
|
|
|
|
local isRoblox = _VERSION == "Luau"
|
|
|
|
|
local isLune = _VERSION:find("Lune") == 1
|
2024-05-25 03:35:35 +01:00
|
|
|
|
local deps = if isLune then require(script.Parent.Parent.deps)else require(script.Parent.deps)
|
|
|
|
|
local requires = deps(isRoblox, isLune)
|
|
|
|
|
local util = require(script.Parent.util)
|
|
|
|
|
|
|
|
|
|
local Signal = util.Signal
|
|
|
|
|
type Signal<T...> = util.Signal<T...>
|
2024-05-24 19:00:33 +01:00
|
|
|
|
|
|
|
|
|
local task = requires.task
|
|
|
|
|
|
2024-05-25 03:35:35 +01:00
|
|
|
|
local result = require(script.Parent.result)
|
2024-05-24 19:00:33 +01:00
|
|
|
|
type Result<T, E> = result.Result<T, E>
|
|
|
|
|
local Ok = result.Ok
|
|
|
|
|
local Err = result.Err
|
|
|
|
|
|
2024-05-25 03:35:35 +01:00
|
|
|
|
local option = require(script.Parent.option)
|
2024-05-24 19:00:33 +01:00
|
|
|
|
type Option<T> = option.Option<T>
|
|
|
|
|
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<Result<string, string>> = 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<string, string> = 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<T>
|
|
|
|
|
@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<T | Result<T, string>, Status> -- Event for internal communication among threads post execution
|
|
|
|
|
@field _status Status -- The status of the Future
|
|
|
|
|
|
|
|
|
|
]=]
|
|
|
|
|
export type Future<T> = typeof(Future) & {
|
|
|
|
|
_ret: T,
|
|
|
|
|
_thread: thread,
|
|
|
|
|
_spawnEvt: Signal<()>,
|
|
|
|
|
_retEvt: Signal<T | Result<T, string>, Status>,
|
|
|
|
|
_status: Status
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
local function _constructor<T>(fn: (Signal<()>, Signal<T, Status>) -> ())
|
|
|
|
|
return setmetatable(
|
|
|
|
|
{
|
|
|
|
|
_thread = coroutine.create(fn),
|
|
|
|
|
|
|
|
|
|
_spawnEvt = Signal.new(),
|
|
|
|
|
-- This is a hack to make luau realize that this object and
|
|
|
|
|
-- Future<T> are related
|
|
|
|
|
_retEvt = Signal.new() :: Signal<T | Result<T, string>, Status>,
|
|
|
|
|
|
|
|
|
|
_status = "initialized",
|
|
|
|
|
} :: Future<T>,
|
|
|
|
|
{
|
|
|
|
|
__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<T> -- The constructed future
|
|
|
|
|
]=]
|
|
|
|
|
function Future.new<T>(fn: (...any) -> T, args: { any })
|
|
|
|
|
return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal<T, Status>)
|
|
|
|
|
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<Result<T>> -- The constructed future
|
|
|
|
|
]=]
|
|
|
|
|
function Future.try<T>(fn: (...any) -> T, args: { any })
|
|
|
|
|
return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal<Result<T, string>, Status>)
|
|
|
|
|
spawnEvt:Fire()
|
|
|
|
|
|
|
|
|
|
local ok, ret = pcall(fn, table.unpack(args))
|
|
|
|
|
local res: Result<T, string> = if ok then Ok(ret) else Err(ret)
|
|
|
|
|
retEvt:Fire(res, "ready")
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
--[=[
|
|
|
|
|
@within Future
|
|
|
|
|
|
|
|
|
|
Polls a [Future] to completion.
|
|
|
|
|
|
|
|
|
|
@param self Future<T>
|
|
|
|
|
@return (Status, Option<T>) -- Returns the [Status] and an optional return if completed
|
|
|
|
|
]=]
|
|
|
|
|
function Future.poll<T>(self: Future<T>): (Status, Option<T>)
|
|
|
|
|
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<T>
|
|
|
|
|
]=]
|
|
|
|
|
function Future.cancel<T>(self: Future<T>)
|
|
|
|
|
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<T>
|
|
|
|
|
@return T -- The value returned by the function on completion
|
|
|
|
|
]=]
|
2024-05-25 03:35:35 +01:00
|
|
|
|
function Future.await<T>(self: Future<T>): Option<T>
|
2024-05-24 19:00:33 +01:00
|
|
|
|
while true do
|
|
|
|
|
local status: Status, ret: Option<T> = self:poll()
|
|
|
|
|
|
|
|
|
|
if status == "ready" then
|
|
|
|
|
-- Safe to unwrap, we know it must not be nil
|
2024-05-25 03:35:35 +01:00
|
|
|
|
return ret
|
2024-05-24 19:00:33 +01:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return Future
|