├── pages ├── index.module.css └── index.js ├── stylua.toml ├── .luaurc ├── selene.toml ├── pesde.lock ├── .gitignore ├── scripts ├── install-packages.sh ├── build.sh ├── run-tests.server.luau ├── dev.sh ├── publish-pesde.sh ├── publish-wally.sh └── test.sh ├── selene_definitions.yml ├── src ├── jest.config.luau ├── __tests__ │ ├── inspectError.test.luau │ ├── mapOk.test.luau │ ├── andThen.test.luau │ ├── inspectOk.test.luau │ ├── unwrapOrElse.test.luau │ ├── orElse.test.luau │ ├── joinAll.test.luau │ ├── await.test.luau │ ├── mapError.test.luau │ ├── join.test.luau │ ├── after.test.luau │ ├── poll.test.luau │ ├── chaining.test.luau │ └── ordering.test.luau ├── utils.luau ├── Result.luau ├── Poll.luau ├── init.luau └── Future.luau ├── moonwave.toml ├── default.project.json ├── dev.project.json ├── .darklua.json ├── rokit.toml ├── .vscode └── settings.json ├── pesde.toml ├── wally.toml ├── CONTRIBUTING.md ├── LICENSE ├── docs ├── installation.md ├── intro.mdx └── typechecking.md ├── .moonwave └── custom.css ├── README.md └── wally.lock /pages/index.module.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | syntax = "Luau" 2 | -------------------------------------------------------------------------------- /.luaurc: -------------------------------------------------------------------------------- 1 | { 2 | "languageMode": "strict" 3 | } 4 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "selene_definitions" 2 | 3 | [rules] 4 | global_usage = "allow" 5 | -------------------------------------------------------------------------------- /pesde.lock: -------------------------------------------------------------------------------- 1 | name = "yetanotherclown/luau_futures" 2 | version = "2.0.0-beta.1" 3 | target = "luau" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.rbxlx.lock 2 | /*.rbxl.lock 3 | /*.rbxl 4 | /*.rbxm 5 | 6 | DevPackages/ 7 | dist/ 8 | build/ 9 | 10 | sourcemap.json -------------------------------------------------------------------------------- /scripts/install-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | wally install \ 6 | && wally-package-types --sourcemap sourcemap.json DevPackages/ -------------------------------------------------------------------------------- /selene_definitions.yml: -------------------------------------------------------------------------------- 1 | base: roblox 2 | name: selene_defs 3 | globals: 4 | # override Roblox require style with string requires 5 | require: 6 | args: 7 | - type: string -------------------------------------------------------------------------------- /src/jest.config.luau: -------------------------------------------------------------------------------- 1 | return { 2 | testMatch = { 3 | "**/__tests__/*.(spec|test)", 4 | }, 5 | testPathIgnorePatterns = { 6 | "Packages", 7 | "DevPackages", 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # If Packages aren't installed, install them. 6 | if [ ! -d "DevPackages" ]; then 7 | sh scripts/install-packages.sh 8 | fi 9 | 10 | rojo sourcemap default.project.json -o sourcemap.json 11 | 12 | darklua process --config .darklua.json src/ dist/src 13 | rojo build build.project.json -o Futures.rbxm -------------------------------------------------------------------------------- /moonwave.toml: -------------------------------------------------------------------------------- 1 | title = "Luau Futures" 2 | 3 | [docusaurus] 4 | tagline = "Rust-like futures for Luau" 5 | 6 | [home] 7 | enabled = true 8 | 9 | [footer] 10 | copyright = "Copyright © 2024 YetAnotherClown. Built with Moonwave and Docusaurus" 11 | 12 | [[navbar.items]] 13 | title = "Discord" 14 | href = "https://discord.gg/nKCV5fjEvH" 15 | position = "right" 16 | className = "discord-logo-link" 17 | -------------------------------------------------------------------------------- /scripts/run-tests.server.luau: -------------------------------------------------------------------------------- 1 | _G.NOCOLOR = _G.NOCOLOR 2 | 3 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 4 | local Packages = ReplicatedStorage.Packages 5 | 6 | local Jest = require("@DevPackages/Jest") 7 | 8 | local runCLIOptions = { 9 | verbose = true, 10 | ci = false, 11 | } 12 | 13 | local projects = { 14 | Packages.Project, 15 | } 16 | 17 | Jest.runCLI(script, runCLIOptions, projects):await() 18 | 19 | return nil 20 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luau-futures", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "DevPackages": { 7 | "$path": "DevPackages" 8 | }, 9 | "Packages": { 10 | "$className": "Folder", 11 | "Project": { 12 | "$path": "src" 13 | } 14 | } 15 | }, 16 | "ServerScriptService": { 17 | "run-tests": { 18 | "$path": "scripts/run-tests.server.luau" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dev.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "luau-futures", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "DevPackages": { 7 | "$path": "DevPackages" 8 | }, 9 | "Packages": { 10 | "$className": "Folder", 11 | "Project": { 12 | "$path": "dist/src" 13 | } 14 | } 15 | }, 16 | "ServerScriptService": { 17 | "run-tests": { 18 | "$path": "dist/run-tests.server.luau" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.darklua.json: -------------------------------------------------------------------------------- 1 | { 2 | "process": [ 3 | { 4 | "rule": "convert_require", 5 | "current": { 6 | "name": "path", 7 | "sources": { 8 | "@DevPackages": "DevPackages/" 9 | } 10 | }, 11 | "target": { 12 | "name": "roblox", 13 | "rojo_sourcemap": "sourcemap.json", 14 | "indexing_style": "wait_for_child" 15 | } 16 | }, 17 | { 18 | "rule": "inject_global_value", 19 | "identifier": "NOCOLOR", 20 | "env": "NOCOLOR" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /scripts/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # If Packages aren't installed, install them. 6 | if [ ! -d "DevPackages" ]; then 7 | sh scripts/install-packages.sh 8 | fi 9 | 10 | rojo serve dev.project.json \ 11 | & rojo sourcemap default.project.json -o sourcemap.json --watch \ 12 | & darklua process --config .darklua.json --watch src/ dist/src \ 13 | & NOCOLOR=1 darklua process --config .darklua.json --watch scripts/run-tests.server.luau dist/run-tests.server.luau -------------------------------------------------------------------------------- /rokit.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Rokit, a toolchain manager for Roblox projects. 2 | # For more information, see https://github.com/rojo-rbx/rokit 3 | 4 | # New tools can be added by running `rokit add ` in a terminal. 5 | 6 | [tools] 7 | rojo = "rojo-rbx/rojo@7.4.4" 8 | run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" 9 | wally = "upliftGames/wally@0.3.2" 10 | darklua = "seaofvoices/darklua@0.14.0" 11 | wally-package-types = "JohnnyMorganz/wally-package-types@1.3.2" 12 | stylua = "JohnnyMorganz/StyLua@2.0.1" 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "luau-lsp.completion.imports.enabled": true, 4 | "luau-lsp.completion.imports.suggestServices": true, 5 | "luau-lsp.completion.imports.suggestRequires": false, 6 | "luau-lsp.require.mode": "relativeToFile", 7 | "luau-lsp.require.directoryAliases": { 8 | "@DevPackages": "DevPackages/" 9 | }, 10 | "luau-lsp.ignoreGlobs": [ 11 | "DevPackages/*", 12 | "dist/*", 13 | ], 14 | "stylua.targetReleaseVersion": "latest" 15 | } -------------------------------------------------------------------------------- /scripts/publish-pesde.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | rojo sourcemap default.project.json -o sourcemap.json 6 | 7 | rm -rf dist 8 | mkdir dist 9 | 10 | cp -r src dist/src 11 | cp README.md dist/README.md 12 | cp LICENSE dist/LICENSE 13 | cp pesde.toml dist/pesde.toml 14 | cp pesde.lock dist/pesde.lock 15 | 16 | cp build.project.json dist/default.project.json 17 | sed -i 's/dist\/src/src/' dist/default.project.json 18 | 19 | cd ./dist 20 | 21 | if [ "$1" = "--publish" ]; then 22 | pesde publish 23 | else 24 | pesde publish -d 25 | fi -------------------------------------------------------------------------------- /scripts/publish-wally.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | rojo sourcemap default.project.json -o sourcemap.json 6 | 7 | rm -rf dist 8 | darklua process --config .darklua.json src/ dist/src 9 | 10 | cp README.md dist/README.md 11 | cp LICENSE dist/LICENSE 12 | cp wally.toml dist/wally.toml 13 | cp wally.lock dist/wally.lock 14 | 15 | cp build.project.json dist/default.project.json 16 | sed -i 's/dist\/src/src/' dist/default.project.json 17 | 18 | cd ./dist 19 | 20 | if [ "$1" = "--publish" ]; then 21 | wally publish 22 | else 23 | wally package --output release.zip 24 | fi -------------------------------------------------------------------------------- /pesde.toml: -------------------------------------------------------------------------------- 1 | name = "yetanotherclown/luau_futures" 2 | version = "2.0.0-beta.3" 3 | description = "Rust-like futures for Luau" 4 | authors = ["YetAnotherClown"] 5 | repository = "https://github.com/YetAnotherClown/luau-futures" 6 | license = "MIT" 7 | includes = [ 8 | "!src/__*__", 9 | "!src/__*__/*", 10 | "!src/jest.config.luau", 11 | "src", 12 | "pesde.lock", 13 | "README.md", 14 | ] 15 | 16 | [target] 17 | environment = "luau" 18 | lib = "src/init.luau" 19 | build_files = ["src"] 20 | 21 | [indices] 22 | default = "https://github.com/daimond113/pesde-index" 23 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | OUTPUT=JestLuaProject.rbxl 6 | 7 | # If Packages aren't installed, install them. 8 | if [ ! -d "DevPackages" ]; then 9 | sh scripts/install-packages.sh 10 | fi 11 | 12 | rojo sourcemap default.project.json -o sourcemap.json \ 13 | && darklua process --config .darklua.json src/ dist/src \ 14 | && darklua process --config .darklua.json scripts/run-tests.server.luau dist/run-tests.server.luau \ 15 | && rojo build dev.project.json --output $OUTPUT \ 16 | && run-in-roblox --place $OUTPUT --script dist/run-tests.server.luau -------------------------------------------------------------------------------- /src/__tests__/inspectError.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("inspectErr", function() 10 | local myFuture = Future.new(function() 11 | error("A", 0) 12 | end) 13 | 14 | myFuture:inspectErr(function(...) 15 | expect(...).toEqual("A") 16 | end) 17 | 18 | local result = myFuture:await() 19 | expect(result:isErr()).toBe(true) 20 | 21 | expect(result:unwrapErr()).toEqual("A") 22 | expect.assertions(3) 23 | end) 24 | -------------------------------------------------------------------------------- /src/__tests__/mapOk.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("mapOk", function() 10 | local myFuture = Future.new(function() 11 | return "a", "b", "c" 12 | end) 13 | 14 | myFuture:mapOk(function(a, b, c) 15 | return a == "a" and 1, b == "b" and 2, c == "c" and 3 16 | end) 17 | 18 | local result = myFuture:await() 19 | expect(result:isOk()).toBe(true) 20 | 21 | local values = { result:unwrapOk() } 22 | 23 | expect(values).toEqual({ 1, 2, 3 }) 24 | end) 25 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yetanotherclown/luau-futures" 3 | description = "Rust-like futures for Luau" 4 | version = "2.0.0-beta.3" 5 | license = "MIT" 6 | authors = ["YetAnotherClown"] 7 | registry = "https://github.com/UpliftGames/wally-index" 8 | realm = "shared" 9 | 10 | # publish-wally.sh will clone to /dist and run wally publish in that directory 11 | exclude = [ 12 | "tests", 13 | "tests/*", 14 | "src/__*__", 15 | "src/__*__/*", 16 | "package.json", 17 | "*.tgz", 18 | "*.zip", 19 | ] 20 | include = ["wally.lock"] 21 | 22 | [dev-dependencies] 23 | Jest = "jsdotlua/jest@3.6.1-rc.2" 24 | JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2" 25 | -------------------------------------------------------------------------------- /src/__tests__/andThen.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("andThen", function() 10 | local myFuture = Future.new(function() 11 | return "a", "b", "c" 12 | end) 13 | 14 | local nextFuture = myFuture:andThen(function() 15 | return Future.new(function() 16 | return 1, 2, 3 17 | end) 18 | end) 19 | 20 | local result = nextFuture:await() 21 | expect(result:isOk()).toBe(true) 22 | 23 | local values = { result:unwrapOk() } 24 | 25 | expect(values).toEqual({ 1, 2, 3 }) 26 | end) 27 | -------------------------------------------------------------------------------- /src/__tests__/inspectOk.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("inspectOk", function() 10 | expect.assertions(3) 11 | 12 | local myFuture = Future.new(function() 13 | return "a", "b", "c" 14 | end) 15 | 16 | myFuture:inspectOk(function(...) 17 | expect({ ... }).toEqual({ "a", "b", "c" }) 18 | end) 19 | 20 | local result = myFuture:await() 21 | expect(result:isOk()).toBe(true) 22 | 23 | local values = { result:unwrapOk() } 24 | 25 | expect(values).toEqual({ "a", "b", "c" }) 26 | end) 27 | -------------------------------------------------------------------------------- /src/__tests__/unwrapOrElse.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("unwrapOrElse", function() 10 | local myFuture = Future.new(function() 11 | error("A", 0) 12 | end) 13 | 14 | myFuture = myFuture:unwrapOrElse(function(err) 15 | expect(err).toEqual("A") 16 | return 1, 2, 3 17 | end) 18 | 19 | local result = myFuture:await() 20 | expect(result:isOk()).toBe(true) 21 | 22 | local results = { result:unwrapOk() } 23 | expect(results).toEqual({ 1, 2, 3 }) 24 | 25 | expect.assertions(3) 26 | end) 27 | -------------------------------------------------------------------------------- /src/__tests__/orElse.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("orElse", function() 10 | local myFuture = Future.new(function() 11 | error("A", 0) 12 | end) 13 | 14 | myFuture = myFuture:orElse(function(err) 15 | expect(err).toEqual("A") 16 | return Future.new(function() 17 | return 1, 2, 3 18 | end) 19 | end) 20 | 21 | local result = myFuture:await() 22 | expect(result:isOk()).toBe(true) 23 | 24 | local results = { result:unwrapOk() } 25 | expect(results).toEqual({ 1, 2, 3 }) 26 | 27 | expect.assertions(3) 28 | end) 29 | -------------------------------------------------------------------------------- /src/__tests__/joinAll.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("joinAll", function() 10 | local myFuture = Future.new(function() 11 | return "a", "b", "c" 12 | end) 13 | 14 | local nextFuture = myFuture:joinAll( 15 | Future.new(function() 16 | return "d", "e", "f" 17 | end), 18 | Future.new(function() 19 | return "g", "h", "i" 20 | end), 21 | Future.new(function() 22 | return "j", "k", "l" 23 | end) 24 | ) 25 | 26 | local result = nextFuture:await() 27 | expect(result:isOk()).toBe(true) 28 | 29 | local values = { result:unwrapOk() } 30 | 31 | -- stylua: ignore 32 | expect(values).toEqual({ 33 | "a", "b", "c", 34 | "d", "e", "f", 35 | "g", "h", "i", 36 | "j", "k", "l", 37 | }) 38 | end) 39 | -------------------------------------------------------------------------------- /src/__tests__/await.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local describe = JestGlobals.describe 4 | local test = JestGlobals.test 5 | local expect = JestGlobals.expect 6 | 7 | local Futures = require("../init") 8 | local Future = Futures.Future 9 | 10 | describe("await", function() 11 | test("immediate", function() 12 | local myFuture = Future.new(function() 13 | return "a", "b", "c" 14 | end) 15 | 16 | local result = myFuture:await() 17 | expect(result:isOk()).toBe(true) 18 | 19 | local values = { result:unwrapOk() } 20 | 21 | expect(values).toEqual({ "a", "b", "c" }) 22 | end) 23 | 24 | test("yielding", function() 25 | local myFuture = Future.new(function() 26 | wait() 27 | return "a", "b", "c" 28 | end) 29 | 30 | local result = myFuture:await() 31 | expect(result:isOk()).toBe(true) 32 | 33 | local values = { result:unwrapOk() } 34 | 35 | expect(values).toEqual({ "a", "b", "c" }) 36 | end) 37 | end) 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Running Tests 4 | 5 | This example has [run-in-roblox](https://github.com/rojo-rbx/run-in-roblox) setup to allow you to run tests from the CLI. 6 | To do so, run the `scripts/test.sh` script and it will open up studio and run your tests. 7 | 8 | If you do not wish to use `run-in-roblox`, you can serve the project with Rojo by running the `scripts/dev.sh`. 9 | Your tests will run and output the results when you run the server in Studio. 10 | 11 | ## Project Structure 12 | 13 | You can find our `run-tests.luau` script in the `scripts` folder. 14 | This is where we define our runCLI Options and our project directories for Jest. 15 | 16 | The `jest.config.luau` file can be found in `src`, this is where we tell Jest what should be considered a test and other options. 17 | 18 | The rest of the project has been setup for use with Darklua and String Requires, and provides scripts to make it simple to use. 19 | The structure is based on [roblox-project-template](https://github.com/grilme99/roblox-project-template), 20 | which provides a setup for a Roblox experience with Darklua and more. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 YetAnotherClown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: How to install Luau Futures 4 | sidebar_position: 2 5 | --- 6 | 7 | Luau Futures has support for any Luau environment, relying only 8 | on the coroutine library. 9 | 10 | We support installing the library from either wally or pesde, with wally 11 | being used internally for development. 12 | 13 | ## Wally 14 | 15 | ```toml 16 | [dependencies] 17 | Futures = "yetanotherclown/luau-futures@^2.0.0-beta.3" 18 | ``` 19 | 20 | :::note 21 | Wally does not export types automatically and will display a type-error in one of the Dependencies. 22 | 23 | To fix this, see https://github.com/JohnnyMorganz/wally-package-types. 24 | ::: 25 | 26 | ## Pesde 27 | 28 | ```toml 29 | [dependencies] 30 | Futures = { name = "yetanotherclown/luau_futures", version = "^2.0.0-beta.3", target = "luau" } 31 | ``` 32 | 33 | Or, you can run `pesde add yetanotherclown/luau_futures --target luau --alias Futures`. 34 | 35 | ## GitHub Releases 36 | 37 | You can also find .rbxm files for use within Studio directly 38 | within GitHub Releases. 39 | 40 | Get the [Latest Release](https://github.com/YetAnotherClown/luau-futures/releases/latest). -------------------------------------------------------------------------------- /src/__tests__/mapError.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local describe = JestGlobals.describe 4 | local test = JestGlobals.test 5 | local expect = JestGlobals.expect 6 | 7 | local Futures = require("../init") 8 | local Future = Futures.Future 9 | 10 | describe("mapErr", function() 11 | test("to error", function() 12 | local myFuture = Future.new(function() 13 | error("A", 0) 14 | end) 15 | 16 | myFuture:mapErr(function(err) 17 | expect(err).toEqual("A") 18 | error("B", 0) 19 | end) 20 | 21 | local result = myFuture:await() 22 | expect(result:isErr()).toBe(true) 23 | 24 | local err = result:unwrapErr() 25 | expect(err).toEqual("B") 26 | expect.assertions(3) 27 | end) 28 | 29 | test("to string", function() 30 | local myFuture = Future.new(function() 31 | error("A", 0) 32 | return "A" 33 | end) 34 | 35 | myFuture:mapErr(function(err) 36 | expect(err).toEqual("A") 37 | return "B" 38 | end) 39 | 40 | local result = myFuture:await() 41 | expect(result:isErr()).toBe(true) 42 | 43 | local err = result:unwrapErr() 44 | expect(err).toEqual("B") 45 | expect.assertions(3) 46 | end) 47 | end) 48 | -------------------------------------------------------------------------------- /src/__tests__/join.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local describe = JestGlobals.describe 4 | local test = JestGlobals.test 5 | local expect = JestGlobals.expect 6 | 7 | local Futures = require("../init") 8 | local Future = Futures.Future 9 | 10 | describe("join", function() 11 | test("ok", function() 12 | local myFuture = Future.new(function() 13 | return "a", "b", "c" 14 | end) 15 | 16 | local nextFuture = myFuture:join(Future.new(function() 17 | return "d", "e", "f" 18 | end)) 19 | 20 | local result = nextFuture:await() 21 | expect(result:isOk()).toBe(true) 22 | 23 | local values = result:unwrapOk() 24 | 25 | expect(values).toEqual({ "a", "b", "c", "d", "e", "f" }) 26 | end) 27 | 28 | test("err", function() 29 | local myFuture = Future.new(function() 30 | error("A", 0) 31 | end) 32 | 33 | local nextFuture = myFuture:join(Future.new(function() 34 | error("B", 0) 35 | end)) 36 | 37 | local result = nextFuture:await() 38 | expect(result:isOk()).toBe(true) 39 | 40 | local values = result:unwrapErr() 41 | 42 | expect(values[1]:unwrapErr()).toEqual("A") 43 | expect(values[2]:unwrapErr()).toEqual("B") 44 | end) 45 | end) 46 | -------------------------------------------------------------------------------- /src/__tests__/after.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local describe = JestGlobals.describe 4 | local test = JestGlobals.test 5 | local expect = JestGlobals.expect 6 | local jest = JestGlobals.jest 7 | 8 | local Futures = require("../init") 9 | local Future = Futures.Future 10 | 11 | describe("after", function() 12 | test("success", function() 13 | local myFuture = Future.new(function() 14 | return "a", "b", "c" 15 | end) 16 | 17 | local _mockSuccess, onSuccess = nil, function() 18 | return 1, 2, 3 19 | end 20 | 21 | local mockFailure, onFailure = jest.fn(function() 22 | error("This should never be called") 23 | end) 24 | 25 | local nextFuture = myFuture:after(function(result) 26 | if result:isOk() then 27 | return Future.new(onSuccess) 28 | else 29 | return Future.new(onFailure :: any) 30 | end 31 | end) 32 | 33 | local result = nextFuture:await() 34 | expect(result:isOk()).toBe(true) 35 | 36 | -- FUTURE: use jest.spyOn 37 | -- expect(mockSuccess).toHaveBeenCalled() 38 | expect(mockFailure).never.toHaveBeenCalled() 39 | 40 | local values = { result:unwrapOk() } 41 | expect(values).toEqual({ 1, 2, 3 }) 42 | end) 43 | end) 44 | -------------------------------------------------------------------------------- /src/__tests__/poll.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local describe = JestGlobals.describe 4 | local test = JestGlobals.test 5 | local expect = JestGlobals.expect 6 | 7 | local Futures = require("../init") 8 | local Future = Futures.Future 9 | 10 | describe("poll", function() 11 | test("immediate", function() 12 | local myFuture = Future.new(function() 13 | return "a", "b", "c" 14 | end) 15 | 16 | local poll = myFuture:poll() 17 | expect(poll:isReady()).toBe(true) 18 | 19 | local result = poll:unwrap() 20 | expect(result:isOk()).toBe(true) 21 | 22 | local values = { result:unwrapOk() } 23 | 24 | expect(values).toEqual({ "a", "b", "c" }) 25 | end) 26 | 27 | test("yielding", function() 28 | local wait = wait 29 | local myFuture = Future.new(function() 30 | wait(0.1) 31 | return "a", "b", "c" 32 | end) 33 | 34 | local poll = myFuture:poll() 35 | expect(poll:isPending()).toBe(true) 36 | 37 | wait(0.1) 38 | 39 | poll = myFuture:poll() 40 | expect(poll:isReady()).toBe(true) 41 | 42 | local result = poll:unwrap() 43 | expect(result:isOk()).toBe(true) 44 | 45 | local values = { result:unwrapOk() } 46 | 47 | expect(values).toEqual({ "a", "b", "c" }) 48 | end) 49 | end) 50 | -------------------------------------------------------------------------------- /src/__tests__/chaining.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | 6 | local Futures = require("../init") 7 | local Future = Futures.Future 8 | 9 | test("chaining", function() 10 | expect.assertions(6) 11 | 12 | local myFuture = Future.new(function() 13 | return 1, 2, 3 14 | end) 15 | :andThen(function(a, b, c) 16 | return Future.new(function() 17 | expect({ a, b, c }).toEqual({ 1, 2, 3 }) 18 | return tostring(a), tostring(b), tostring(c) 19 | end) 20 | end) 21 | :inspectOk(function(a, b, c) 22 | expect({ a, b, c }).toEqual({ "1", "2", "3" }) 23 | end) 24 | :mapErr(function(_err) 25 | return "I don't expect an error here" 26 | end) 27 | :mapOk(function(a, b, c) 28 | expect({ a, b, c }).toEqual({ "1", "2", "3" }) 29 | return false, true, 22 30 | end) 31 | :inspectOk(function(a, b, c) 32 | expect({ a, b, c }).toEqual({ false, true, 22 } :: { any }) 33 | end) 34 | :orElse(function(_err) 35 | return Future.new(function() 36 | return false, true, 21 37 | end) :: any 38 | end) 39 | 40 | local result = myFuture:await() 41 | expect(result:isOk()).toBe(true) 42 | 43 | local values = { result:unwrapOk() } 44 | expect(values).toEqual({ false, true, 22 } :: { any }) 45 | end) 46 | -------------------------------------------------------------------------------- /src/__tests__/ordering.test.luau: -------------------------------------------------------------------------------- 1 | local JestGlobals = require("@DevPackages/JestGlobals") 2 | 3 | local test = JestGlobals.test 4 | local expect = JestGlobals.expect 5 | local describe = JestGlobals.describe 6 | 7 | local Futures = require("../init") 8 | local Future = Futures.Future 9 | 10 | describe("ordering", function() 11 | test("chaining", function() 12 | expect.assertions(7) 13 | 14 | local callOrder = {} 15 | 16 | local myFuture = Future.new(function() 17 | table.insert(callOrder, "A") 18 | return 1, 2, 3 19 | end) 20 | :andThen(function(a, b, c) 21 | return Future.new(function() 22 | table.insert(callOrder, "B") 23 | expect({ a, b, c }).toEqual({ 1, 2, 3 }) 24 | return tostring(a), tostring(b), tostring(c) 25 | end) 26 | end) 27 | :inspectOk(function(a, b, c) 28 | table.insert(callOrder, "C") 29 | expect({ a, b, c }).toEqual({ "1", "2", "3" }) 30 | end) 31 | :mapErr(function(_err) 32 | table.insert(callOrder, "Should not be called") 33 | return "I don't expect an error here" 34 | end) 35 | :mapOk(function(a, b, c) 36 | table.insert(callOrder, "D") 37 | expect({ a, b, c }).toEqual({ "1", "2", "3" }) 38 | return false, true, 22 39 | end) 40 | :inspectOk(function(a, b, c) 41 | table.insert(callOrder, "E") 42 | expect({ a, b, c }).toEqual({ false, true, 22 } :: { any }) 43 | end) 44 | :orElse(function(_err) 45 | table.insert(callOrder, "Should not be called") 46 | return Future.new(function() 47 | return false, true, 21 48 | end) :: any 49 | end) 50 | 51 | local result = myFuture:await() 52 | expect(result:isOk()).toBe(true) 53 | 54 | local values = { result:unwrapOk() } 55 | expect(values).toEqual({ false, true, 22 } :: { any }) 56 | 57 | expect(callOrder).toEqual({ "A", "B", "C", "D", "E" }) 58 | end) 59 | end) 60 | -------------------------------------------------------------------------------- /docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Luau Futures 3 | description: An introduction to Luau Futures 4 | sidebar_position: 1 5 | --- 6 | 7 | import Tabs from '@theme/Tabs'; 8 | import TabItem from '@theme/TabItem'; 9 | 10 | A future represents a read-only asynchronous value, one that may 11 | not have finished computation yet. A basic future could look like: 12 | 13 | ```lua 14 | local Futures = require("@packages/Futures") 15 | local Future = Futures.Future 16 | 17 | local myFuture = Future.new(function() 18 | yield() 19 | return 1, 2, 3 20 | end) 21 | ``` 22 | 23 | When you create a future, it wont begin execution until it is either 24 | polled or awaited. 25 | 26 | Polling will advance the future to it's next resumption point every 27 | time that it is called, returning a [Poll] to let you check the status 28 | of the future. 29 | 30 | If the Poll is ready, you can also unwrap it to get the [Result]. 31 | ```lua 32 | local poll = myFuture:poll() 33 | if poll:isReady() then 34 | local result = poll:unwrap() 35 | -- Handle result 36 | end 37 | ``` 38 | 39 | Awaiting a future will yield the current thread until the future 40 | finishes execution. As such, it is recommended that you only use 41 | the await method within other futures, preferring to use poll instead. 42 | 43 | ```lua 44 | local result = myFuture:await() 45 | -- Handle result 46 | ``` 47 | 48 | To read the result, you can use [Result:isOk] or [Result:isErr] to 49 | check what type the Result is. 50 | 51 | You can then use [Result:unwrapOk] or [Result:unwrapErr] to get the 52 | value of the result. 53 | 54 | ```lua 55 | if result:isOk() then 56 | print(result:unwrapOk()) -- 1, 2, 3 57 | elseif result:isErr() then 58 | warn(result:unwrapErr()) -- An error occurred 59 | end 60 | ``` 61 | 62 | There are also several other methods for chaining, combining, and 63 | mapping futures, as well as other utilities for working with futures. 64 | 65 | It is suggested to read the [API Documentation](/api/Future) for more 66 | information about these methods. -------------------------------------------------------------------------------- /docs/typechecking.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Typechecking Guide 3 | description: A guide on Typechecking Futures 4 | --- 5 | 6 | Currently, in the Luau type solver, there are restrictions placed on recursive types. Luau Futures works around this, by allowing you to chain up 7 | to 3 recursive functions (andThen, map, join, etc.) before the types are exhausted. 8 | 9 | ## Future vs FutureLike Types 10 | 11 | The Futures library exports two types because of these restrictions. 12 | `FutureLike` should be used when your being given a future, such as in a 13 | function with a future as a parameter. 14 | The `Future` type should be used when returning a future, such as in a 15 | function return. 16 | 17 | Example usage, 18 | ```lua 19 | function Class:method(future: Futures.FutureLike): Futures.Future 20 | return future:andThen(function(...) 21 | -- ... 22 | end) :: any 23 | end 24 | ``` 25 | 26 | :::note 27 | To avoid recursive type restrictions, there are internally multiple types like 28 | `FutureFirst`, `FutureNext`, `FutureLast` and `FutureExhausted`. 29 | 30 | The `Futures.Future` type is just `FutureFirst`, so when you use that type it 31 | will expect a `FutureFirst` which is the first type you get when creating 32 | a future with `Future.new()`. 33 | 34 | If you are chaining a future in a function that returns one, you can annotate 35 | the return type to be `Futures.Future` and then typecast the returned future 36 | with `:: any` like in the example. 37 | ::: 38 | 39 | In the Future, `Futures.FutureLike` will be deprecated, but it will remain 40 | available for backwards compatibility. 41 | 42 | ## Join Methods 43 | 44 | Currently, the Luau type solver cannot properly type the join methods. 45 | So for now, they will always return an exhausted future. 46 | 47 | ## UnwrapOrElse 48 | 49 | [Future:unwrapOrElse] should return the type `Future`. However, 50 | due to recursive type restrictions, it will return `Future`. 51 | 52 | You should make a mental note that using this method will never error, and 53 | write your code accordingly. 54 | 55 | ## Getting Around Exhaustion 56 | 57 | So, you have an exhausted future that you want to typecheck. 58 | You can use type annotations and type casts to get around exhausted 59 | futures. 60 | 61 | ```lua 62 | local newFuture: Future = exhaustedFuture:andThen(function(...) 63 | -- ... 64 | return 1, 2, 3 65 | end) :: any 66 | ``` -------------------------------------------------------------------------------- /src/utils.luau: -------------------------------------------------------------------------------- 1 | -- https://stackoverflow.com/questions/7526223/how-do-i-know-if-a-table-is-an-array/52697380#52697380 2 | local function isArray(t) 3 | if type(t) ~= "table" then 4 | return false 5 | end 6 | 7 | if #t > 0 then 8 | return true 9 | end 10 | 11 | for _, _ in pairs(t) do 12 | return false 13 | end 14 | 15 | return true 16 | end 17 | 18 | local function printArray(arr, indentation: number): string 19 | local output = "" 20 | 21 | local indent = "" 22 | for _ = 1, indentation do 23 | indent ..= "\t" 24 | end 25 | 26 | local outerIndent = " " 27 | if #arr ~= 0 and string.len(indent) >= 1 then 28 | outerIndent = string.sub(indent, 1, string.len(indent) - 1) 29 | end 30 | 31 | for i, v in arr do 32 | local vOutput = if type(v) == "table" then prettyPrintTable(v, indentation + 1) else prettyPrint(v) 33 | 34 | output ..= `\n{indent}{vOutput},` 35 | 36 | if i == #arr then 37 | output ..= `\n` 38 | end 39 | end 40 | 41 | return `\{ {output}{outerIndent}\}` 42 | end 43 | 44 | local function printDictionary(dict, indentation: number): string 45 | local output = "" 46 | 47 | local indent = "" 48 | for _ = 1, indentation do 49 | indent ..= "\t" 50 | end 51 | 52 | local outerIndent = "" 53 | if string.len(indent) >= 1 then 54 | outerIndent = string.sub(indent, 1, string.len(indent) - 1) 55 | end 56 | 57 | for k, v in dict do 58 | local kOutput = if type(k) == "table" then prettyPrintTable(k, indentation + 1) else prettyPrint(k) 59 | local vOutput = if type(v) == "table" then prettyPrintTable(v, indentation + 1) else prettyPrint(v) 60 | 61 | output ..= `\n{indent}[{kOutput}] = {vOutput},` 62 | end 63 | 64 | return `\{ {output} \n{outerIndent}\}` 65 | end 66 | 67 | function prettyPrintTable(t: any, indentation: number): string 68 | if isArray(t) then 69 | return printArray(t, indentation) 70 | else 71 | return printDictionary(t, indentation) 72 | end 73 | end 74 | 75 | local function prettyPrintString(str) 76 | local singleFound = string.find(str, "'") 77 | local doubleFound = string.find(str, '"') 78 | 79 | if not doubleFound then 80 | return `"{str}"` 81 | elseif not singleFound then 82 | return `'{str}'` 83 | else 84 | str = string.gsub(str, '"', '\\"') 85 | str = string.gsub(str, "'", "\\'") 86 | return `"{str}"` 87 | end 88 | end 89 | 90 | function prettyPrint(...): string 91 | local output = {} 92 | 93 | for i = 1, select("#", ...) do 94 | local v = select(i, ...) 95 | 96 | if type(v) == "table" then 97 | output[i] = prettyPrintTable(v, 1) 98 | continue 99 | elseif type(v) == "string" then 100 | output[i] = prettyPrintString(v) 101 | continue 102 | end 103 | 104 | output[i] = tostring(v) 105 | end 106 | 107 | return table.concat(output, ", ") 108 | end 109 | 110 | return { 111 | prettyPrint = prettyPrint, 112 | } 113 | -------------------------------------------------------------------------------- /src/Result.luau: -------------------------------------------------------------------------------- 1 | local utils = require("./utils") 2 | local prettyPrint = utils.prettyPrint 3 | 4 | type ResultType = "Ok" | "Err" 5 | 6 | type ResultImpl = { 7 | isOk: (self: Result) -> boolean, 8 | isErr: (self: Result) -> boolean, 9 | unwrapOk: (self: Result) -> U..., 10 | unwrapErr: (self: Result) -> E, 11 | } 12 | 13 | --- @class Result 14 | --- 15 | --- A Result type for use with Futures, with API designed 16 | --- to support strict typing. 17 | local Result = {} 18 | Result.__index = Result 19 | 20 | function Result:__tostring() 21 | local results = prettyPrint(table.unpack(self._results)) 22 | 23 | if self:isOk() then 24 | return `Result::Ok({results})` 25 | elseif self:isErr() then 26 | return `Result::Err({results})` 27 | end 28 | 29 | return "Result::Unknown" 30 | end 31 | 32 | export type Result = { 33 | _type: ResultType, 34 | _results: { any }, 35 | } & ResultImpl 36 | 37 | --- @within Result 38 | --- 39 | --- Checks whether the Result is of the `Ok` type. 40 | function Result:isOk(): boolean 41 | return self._type == "Ok" 42 | end 43 | 44 | --- @within Result 45 | --- 46 | --- Checks whether the Result is of the `Err` type. 47 | function Result:isErr(): boolean 48 | return self._type == "Err" 49 | end 50 | 51 | --- @within Result 52 | --- 53 | --- Unwraps the results of an `Ok` Result. 54 | --- 55 | --- :::danger 56 | --- Make sure you use [Result:isOk] before using this method. 57 | --- 58 | --- ```lua 59 | --- if result:isOk() then 60 | --- local result = result:unwrapOk() 61 | --- end 62 | --- ``` 63 | --- ::: 64 | function Result:unwrapOk(): U... 65 | return table.unpack(self._results :: any) 66 | end 67 | 68 | --- @within Result 69 | --- 70 | --- Unwraps the results of an `Err` Result. 71 | --- 72 | --- :::danger 73 | --- Make sure you use [Result:isErr] before using this method. 74 | --- 75 | --- ```lua 76 | --- if result:isErr() then 77 | --- local err = result:unwrapErr() 78 | --- end 79 | --- ``` 80 | --- ::: 81 | function Result:unwrapErr(): E 82 | return table.unpack(self._results) 83 | end 84 | 85 | --- @within Result 86 | --- 87 | --- Creates a new Result of the `Ok` type. 88 | function Result.ok(...: U...): Result 89 | local result = setmetatable({ 90 | _type = "Ok", 91 | _results = table.pack(...), 92 | }, Result) :: any 93 | 94 | return result 95 | end 96 | 97 | --- @within Result 98 | --- 99 | --- Creates a new Result of the `Err` type. 100 | function Result.err(errValue: E): Result 101 | local result = setmetatable({ 102 | _type = "Err", 103 | _results = { errValue }, 104 | }, Result) :: any 105 | 106 | return result 107 | end 108 | 109 | type Library = { 110 | ok: (U...) -> Result, 111 | err: (E) -> Result, 112 | } 113 | 114 | return (Result :: any) :: Library 115 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from "@docusaurus/Link"; 2 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 3 | import Layout from "@theme/Layout"; 4 | import clsx from "clsx"; 5 | import React from "react"; 6 | import styles from "./index.module.css"; 7 | 8 | const FEATURES = [ 9 | { 10 | title: "Lazy, not Eager.", 11 | description: ( 12 | <> 13 |

Futures will not begin execution until polled or awaited, unlike Promises when begin immediately.

14 |

15 | Polling will execute until the next suspension point, until execution is finished. By awaiting a 16 | Future, it will yield the current thread until execution has completed. 17 |

18 | 19 | ), 20 | }, 21 | { 22 | title: "Rusty. Rust-like.", 23 | description: ( 24 | <> 25 | Luau Futures aim to have similar API and behavior to Rust Futures where possible, drawing inspiration 26 | otherwise. 27 | 28 | ), 29 | }, 30 | { 31 | title: "Types? Check. Typechecked.", 32 | description: ( 33 | <> 34 | Luau Futures are strictly typed, within the type solver's restrictions. 35 | 40 | 41 | ), 42 | }, 43 | ]; 44 | 45 | function FeatureIcon({ icon }) { 46 | return
{icon}
; 47 | } 48 | 49 | function Feature({ title, description }) { 50 | return ( 51 |
52 |
53 |
54 |

{title}

55 |
56 |
{description}
57 |
58 |
59 | ); 60 | } 61 | 62 | function HomepageFeatures() { 63 | return ( 64 |
65 |
66 | {FEATURES.map((props, idx) => ( 67 | 68 | ))} 69 |
70 |
71 | ); 72 | } 73 | 74 | function HeroBanner() { 75 | const { siteConfig } = useDocusaurusContext(); 76 | 77 | return ( 78 |
79 |
80 |

81 | {siteConfig.title} 82 |

83 |

84 | {siteConfig.tagline} 85 |

86 |
87 | 88 | Get Started 89 | 90 | 95 | API 96 | 97 |
98 |
99 |
100 | ); 101 | } 102 | 103 | export default function Homepage() { 104 | const { siteConfig } = useDocusaurusContext(); 105 | return ( 106 | 107 |
108 | 109 | 110 |
111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/Poll.luau: -------------------------------------------------------------------------------- 1 | local Result = require("./Result") 2 | type Result = Result.Result 3 | 4 | type PollImpl = { 5 | isReady: (self: Poll) -> boolean, 6 | isPending: (self: Poll) -> boolean, 7 | unwrap: (self: Poll) -> Result, 8 | } 9 | 10 | --- @class Poll 11 | --- 12 | --- A Poll represents the status of a [Result], 13 | --- whether that result is Ready or Pending. 14 | --- 15 | --- ```lua 16 | --- local poll = Poll.ok(...) 17 | --- 18 | --- if poll:isReady() then 19 | --- local result = poll:unwrap() 20 | --- -- ... 21 | --- elseif poll:isPending() then 22 | --- -- ... 23 | --- end 24 | --- ``` 25 | local Poll = {} 26 | Poll.__index = Poll 27 | 28 | function Poll:__tostring() 29 | if self:isReady() then 30 | local result = self:unwrap() 31 | return `Poll::Ready({result})` 32 | elseif self:isPending() then 33 | return "Poll::Pending" 34 | end 35 | 36 | return "Poll::Unknown" 37 | end 38 | 39 | export type Poll = { 40 | _ready: boolean, 41 | _results: { any }, 42 | _resultType: ("Ok" | "Err")?, 43 | } & PollImpl 44 | 45 | --- @within Poll 46 | --- 47 | --- Checks whether the Result of a Poll is ready. 48 | --- ```lua 49 | --- local poll = future:poll() 50 | --- if poll:isReady() then 51 | --- local result = poll:unwrap() 52 | --- -- ... 53 | --- end 54 | --- ``` 55 | function Poll:isReady(): boolean 56 | return self._ready 57 | end 58 | 59 | --- @within Poll 60 | --- 61 | --- Checks whether the Result of a Poll is pending. 62 | --- ```lua 63 | --- local poll = future:poll() 64 | --- if poll:isPending() then 65 | --- continue 66 | --- end 67 | --- ``` 68 | function Poll:isPending(): boolean 69 | return not self._ready 70 | end 71 | 72 | --- @within Poll 73 | --- @error Attempt to unwrap a pending future -- To prevent this, use Poll:isReady() or Future:await() to ensure the Result is ready. 74 | --- 75 | --- Checks whether the Result of a Poll is ready. 76 | --- ```lua 77 | --- local poll = future:poll() 78 | --- if poll:isReady() then 79 | --- local result = poll:unwrap() 80 | --- -- ... 81 | --- else 82 | --- local result = poll:unwrap() --! Errors 83 | --- -- Error: Attempt to unwrap a pending future 84 | --- end 85 | --- ``` 86 | function Poll:unwrap(): Result 87 | if not self._ready then 88 | error("Attempt to unwrap a pending future, use Poll:isReady() or Future:await() to avoid this error.") 89 | else 90 | if self._resultType == "Ok" then 91 | return Result.ok(table.unpack(self._results)) :: any 92 | else 93 | return Result.err(table.unpack(self._results)) :: any 94 | end 95 | end 96 | end 97 | 98 | --- @within Poll 99 | --- 100 | --- Creates a Poll with an `Ok` result. 101 | function Poll.ok(...: U...): Poll 102 | local poll = setmetatable({ 103 | _ready = true, 104 | _resultType = "Ok", 105 | _results = table.pack(...), 106 | }, Poll) :: any 107 | 108 | return poll 109 | end 110 | 111 | --- @within Poll 112 | --- 113 | --- Creates a Poll with an `Err` result. 114 | function Poll.err(errValue: E): Poll 115 | local poll = setmetatable({ 116 | _ready = true, 117 | _resultType = "Err", 118 | _results = { errValue }, 119 | }, Poll) :: any 120 | 121 | return poll 122 | end 123 | 124 | --- @within Poll 125 | --- 126 | --- Creates a Poll which status is not ready. 127 | function Poll.notReady(): Poll 128 | local poll = setmetatable({ 129 | _ready = false, 130 | _resultType = nil, 131 | _results = {}, 132 | }, Poll) :: any 133 | 134 | return poll 135 | end 136 | 137 | type Library = { 138 | ok: (U...) -> Poll, 139 | err: (errValue: E) -> Poll, 140 | notReady: () -> Poll, 141 | } 142 | 143 | return (Poll :: any) :: Library 144 | -------------------------------------------------------------------------------- /.moonwave/custom.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); 2 | 3 | :root { 4 | --ifm-font-family-base: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, 5 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 6 | --ifm-navbar-background-color: var(--ifm-background-color); 7 | --ifm-navbar-search-input-background-color: var(--ifm-background-color); 8 | --ifm-navbar-shadow: none; 9 | } 10 | 11 | :root[data-theme="light"] { 12 | --ifm-navbar-search-input-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14'/%3E%3C/svg%3E"); 13 | } 14 | 15 | :root[data-theme="dark"] { 16 | --ifm-navbar-search-input-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24'%3E%3Cpath fill='white' d='m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14'/%3E%3C/svg%3E"); 17 | } 18 | 19 | .navbar__search { 20 | margin-left: 0%; 21 | margin-right: 12px; 22 | } 23 | 24 | .navbar__search-input { 25 | width: 0px; 26 | transition: width 500ms, padding 500ms; 27 | outline: none; 28 | background-color: var(--ifm-navbar-search-input-background-color); 29 | background-image: var(--ifm-navbar-search-input-icon); 30 | background-repeat: no-repeat; 31 | background-position: 0% 50%; 32 | background-size: 24px; 33 | padding: 0 24px 0 0; 34 | } 35 | 36 | .navbar__search-input:hover, 37 | .navbar__search-input:focus { 38 | width: 172px; 39 | padding: 0 8px 0 36px; 40 | } 41 | 42 | .discord-logo-link { 43 | background-color: var(--ifm-navbar-link-color); 44 | transition: background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default); 45 | mask-image: url("data:image/svg+xml,%3Csvg fill='%23000000' viewBox='0 0 32 32' version='1.1' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3Ediscord%3C/title%3E%3Cpath d='M20.992 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.050 0.005 0.109 0.005 0.168 0 1.523-1.191 2.768-2.693 2.854l-0.008 0zM11.026 20.163c-1.511-0.099-2.699-1.349-2.699-2.877 0-0.051 0.001-0.102 0.004-0.153l-0 0.007c-0.003-0.048-0.005-0.104-0.005-0.161 0-1.525 1.19-2.771 2.692-2.862l0.008-0c1.509 0.082 2.701 1.325 2.701 2.847 0 0.062-0.002 0.123-0.006 0.184l0-0.008c0.003 0.048 0.005 0.104 0.005 0.161 0 1.525-1.19 2.771-2.692 2.862l-0.008 0zM26.393 6.465c-1.763-0.832-3.811-1.49-5.955-1.871l-0.149-0.022c-0.005-0.001-0.011-0.002-0.017-0.002-0.035 0-0.065 0.019-0.081 0.047l-0 0c-0.234 0.411-0.488 0.924-0.717 1.45l-0.043 0.111c-1.030-0.165-2.218-0.259-3.428-0.259s-2.398 0.094-3.557 0.275l0.129-0.017c-0.27-0.63-0.528-1.142-0.813-1.638l0.041 0.077c-0.017-0.029-0.048-0.047-0.083-0.047-0.005 0-0.011 0-0.016 0.001l0.001-0c-2.293 0.403-4.342 1.060-6.256 1.957l0.151-0.064c-0.017 0.007-0.031 0.019-0.040 0.034l-0 0c-2.854 4.041-4.562 9.069-4.562 14.496 0 0.907 0.048 1.802 0.141 2.684l-0.009-0.11c0.003 0.029 0.018 0.053 0.039 0.070l0 0c2.14 1.601 4.628 2.891 7.313 3.738l0.176 0.048c0.008 0.003 0.018 0.004 0.028 0.004 0.032 0 0.060-0.015 0.077-0.038l0-0c0.535-0.72 1.044-1.536 1.485-2.392l0.047-0.1c0.006-0.012 0.010-0.027 0.010-0.043 0-0.041-0.026-0.075-0.062-0.089l-0.001-0c-0.912-0.352-1.683-0.727-2.417-1.157l0.077 0.042c-0.029-0.017-0.048-0.048-0.048-0.083 0-0.031 0.015-0.059 0.038-0.076l0-0c0.157-0.118 0.315-0.24 0.465-0.364 0.016-0.013 0.037-0.021 0.059-0.021 0.014 0 0.027 0.003 0.038 0.008l-0.001-0c2.208 1.061 4.8 1.681 7.536 1.681s5.329-0.62 7.643-1.727l-0.107 0.046c0.012-0.006 0.025-0.009 0.040-0.009 0.022 0 0.043 0.008 0.059 0.021l-0-0c0.15 0.124 0.307 0.248 0.466 0.365 0.023 0.018 0.038 0.046 0.038 0.077 0 0.035-0.019 0.065-0.046 0.082l-0 0c-0.661 0.395-1.432 0.769-2.235 1.078l-0.105 0.036c-0.036 0.014-0.062 0.049-0.062 0.089 0 0.016 0.004 0.031 0.011 0.044l-0-0.001c0.501 0.96 1.009 1.775 1.571 2.548l-0.040-0.057c0.017 0.024 0.046 0.040 0.077 0.040 0.010 0 0.020-0.002 0.029-0.004l-0.001 0c2.865-0.892 5.358-2.182 7.566-3.832l-0.065 0.047c0.022-0.016 0.036-0.041 0.039-0.069l0-0c0.087-0.784 0.136-1.694 0.136-2.615 0-5.415-1.712-10.43-4.623-14.534l0.052 0.078c-0.008-0.016-0.022-0.029-0.038-0.036l-0-0z'%3E%3C/path%3E%3C/svg%3E"); 46 | mask-repeat: no-repeat; 47 | mask-position: center; 48 | display: flex; 49 | width: 22px; 50 | height: 22px; 51 | margin: 0 6px 0 6px; 52 | order: 5; 53 | } 54 | 55 | .discord-logo-link:hover { 56 | background-color: var(--ifm-navbar-link-hover-color); 57 | } 58 | 59 | .navbar__link[href*="https://github.com/"] 60 | { 61 | background-color: var(--ifm-navbar-link-color); 62 | transition: background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default); 63 | mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); 64 | mask-repeat: no-repeat; 65 | mask-position: center; 66 | display: flex; 67 | width: 20px; 68 | height: 20px; 69 | margin: 0 6px 0 18px; 70 | order: 4; 71 | } 72 | 73 | .navbar__link[href*="https://github.com/"]:hover 74 | { 75 | background-color: var(--ifm-navbar-link-hover-color); 76 | } 77 | 78 | .footer { 79 | --ifm-footer-background-color: var(--ifm-background-color); 80 | --ifm-footer-color: var(--ifm-color-content-secondary); 81 | } 82 | 83 | div[class*="docSidebarContainer"], 84 | aside[class*="docSidebarContainer"] { 85 | border: 0; 86 | } 87 | 88 | .hero-button:not(:hover) { 89 | color: var(--ifm-color-secondary) !important; 90 | } 91 | 92 | div[class*="sourceButtonText"] { 93 | border: 0px; 94 | } 95 | -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | local Poll = require("@self/Poll") 2 | export type Poll = Poll.Poll 3 | 4 | local Result = require("@self/Result") 5 | export type Result = Result.Result 6 | 7 | local Future = require("@self/Future") :: any 8 | 9 | -- FUTURE: FutureLike = Future for backwards compatibility 10 | -- This type should be used to typecheck function parameters,\ 11 | -- using Futures.Future will not work as expected. 12 | export type FutureLike = { 13 | await: (self: FutureLike) -> Result, 14 | poll: (self: FutureLike) -> Poll, 15 | [any]: any, 16 | } 17 | 18 | -- FUTURE: Replace when recursive type restrictions are lifted 19 | -- This type should only be used when returning a Future from a function,\ 20 | -- Using Futures.FutureLike will not provide intellisense. 21 | export type Future = FutureFirst 22 | 23 | type FutureExhausted = { 24 | _onResolve: { (...any) -> () }, 25 | _onErr: { (any) -> () }, 26 | _mapOn: { 27 | ok: { (...any) -> T... }, 28 | err: { (any) -> T }, 29 | }, 30 | _arguments: { any }, 31 | _callback: (...any) -> ...any, 32 | _threadStatus: { success: boolean, results: { any } }, 33 | _thread: thread?, 34 | 35 | after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureExhausted, 36 | andThen: (self: FutureLike, fn: (...any) -> FutureLike) -> FutureExhausted, 37 | await: (self: FutureLike) -> Result, 38 | inspectErr: (self: FutureLike, fn: (any) -> ()) -> FutureExhausted, 39 | inspectOk: (self: FutureLike, fn: (...any) -> ()) -> FutureExhausted, 40 | join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, 41 | joinAll: (...FutureLike) -> FutureExhausted, 42 | mapErr: (self: FutureLike, fn: (any) -> T) -> FutureExhausted, 43 | mapOk: (self: FutureLike, fn: (...any) -> T...) -> FutureExhausted, 44 | orElse: (self: FutureLike, fn: (any) -> FutureExhausted) -> FutureExhausted, 45 | unwrapOrElse: (self: FutureLike, fn: (err: any) -> ...any) -> FutureExhausted, 46 | poll: (self: FutureLike) -> Poll, 47 | } 48 | 49 | type FutureLast = { 50 | _onResolve: { (U...) -> () }, 51 | _onErr: { (E) -> () }, 52 | _mapOn: { 53 | ok: { (U...) -> T... }, 54 | err: { (E) -> T }, 55 | }, 56 | _arguments: { any }, 57 | _callback: (...any) -> U..., 58 | _threadStatus: { success: boolean, results: { any } }, 59 | _thread: thread?, 60 | 61 | after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureExhausted, 62 | andThen: (self: FutureLike, fn: (U...) -> FutureLike) -> FutureExhausted, 63 | await: (self: FutureLike) -> Result, 64 | inspectErr: (self: FutureLike, fn: (E) -> ()) -> FutureLast, 65 | inspectOk: (self: FutureLike, fn: (U...) -> ()) -> FutureLast, 66 | join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, 67 | joinAll: (...FutureLike) -> FutureExhausted, 68 | mapErr: (self: FutureLike, fn: (E) -> T) -> FutureExhausted, 69 | mapOk: (self: FutureLike, fn: (U...) -> T...) -> FutureExhausted, 70 | orElse: (self: FutureLike, fn: (E) -> FutureLast) -> FutureLast, 71 | unwrapOrElse: (self: FutureLike, fn: (err: E) -> U...) -> FutureLast, 72 | poll: (self: FutureLike) -> Poll, 73 | } 74 | 75 | type FutureNext = { 76 | _onResolve: { (U...) -> () }, 77 | _onErr: { (E) -> () }, 78 | _mapOn: { 79 | ok: { (U...) -> T... }, 80 | err: { (E) -> T }, 81 | }, 82 | _arguments: { any }, 83 | _callback: (...any) -> U..., 84 | _threadStatus: { success: boolean, results: { any } }, 85 | _thread: thread?, 86 | 87 | after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureLast, 88 | andThen: (self: FutureLike, fn: (U...) -> FutureLike) -> FutureLast, 89 | await: (self: FutureLike) -> Result, 90 | inspectErr: (self: FutureLike, fn: (E) -> ()) -> FutureNext, 91 | inspectOk: (self: FutureLike, fn: (U...) -> ()) -> FutureNext, 92 | join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, 93 | joinAll: (...FutureLike) -> FutureExhausted, 94 | mapErr: (self: FutureLike, fn: (E) -> T) -> FutureLast, 95 | mapOk: (self: FutureLike, fn: (U...) -> T...) -> FutureLast, 96 | orElse: (self: FutureLike, fn: (E) -> FutureNext) -> FutureNext, 97 | unwrapOrElse: (self: FutureLike, fn: (err: E) -> U...) -> FutureNext, 98 | poll: (self: FutureLike) -> Poll, 99 | } 100 | 101 | type FutureFirst = { 102 | _onResolve: { (U...) -> () }, 103 | _onErr: { (E) -> () }, 104 | _mapOn: { 105 | ok: { (U...) -> T... }, 106 | err: { (E) -> T }, 107 | }, 108 | _arguments: { any }, 109 | _callback: (...any) -> U..., 110 | _threadStatus: { success: boolean, results: { any } }, 111 | _thread: thread?, 112 | 113 | after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureNext, 114 | andThen: (self: FutureLike, fn: (U...) -> FutureLike) -> FutureNext, 115 | await: (self: FutureLike) -> Result, 116 | inspectErr: (self: FutureLike, fn: (E) -> ()) -> FutureFirst, 117 | inspectOk: (self: FutureLike, fn: (U...) -> ()) -> FutureFirst, 118 | -- FUTURE: Return Future 119 | join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, 120 | -- NOTE: This might be a little tricky to type, we can supply join3, join4, join5... methods 121 | joinAll: (...FutureLike) -> FutureExhausted, 122 | mapErr: (self: FutureLike, fn: (E) -> T) -> FutureNext, 123 | mapOk: (self: FutureLike, fn: (U...) -> T...) -> FutureNext, 124 | orElse: (self: FutureLike, fn: (E) -> FutureFirst) -> FutureFirst, 125 | -- FUTURE: Return Future 126 | unwrapOrElse: (self: FutureLike, fn: (err: E) -> U...) -> FutureFirst, 127 | poll: (self: FutureLike) -> Poll, 128 | } 129 | 130 | type Library = { 131 | Poll: typeof(Poll), 132 | Result: typeof(Result), 133 | 134 | -- FUTURE: Replace when recursive type restrictions are lifted 135 | Future: { 136 | new: (callback: (T...) -> U..., T...) -> FutureFirst, 137 | [any]: any, 138 | }, 139 | } 140 | 141 | return { 142 | Poll = Poll, 143 | Result = Result, 144 | 145 | Future = Future, 146 | } :: Library 147 | -------------------------------------------------------------------------------- /src/Future.luau: -------------------------------------------------------------------------------- 1 | local Poll = require("./Poll") 2 | export type Poll = Poll.Poll 3 | 4 | local Result = require("./Result") 5 | export type Result = Result.Result 6 | 7 | --- @class Future 8 | --- 9 | --- A Future represents a read-only asynchronous value, one that may not 10 | --- have finished computation. 11 | --- 12 | --- Futures are lazy in their computation, meaning execution will not begin 13 | --- until [Future:poll] or [Future:await] is used. 14 | --- 15 | --- ## Types 16 | --- 17 | --- You should refer to the [Typechecking Guide](/docs/typechecking) for more information. 18 | --- 19 | --- ### Future 20 | --- 21 | --- This type should only be used when returning a Future from a function, 22 | --- using Futures.FutureLike will not provide intellisense. 23 | --- 24 | --- ### FutureLike 25 | --- 26 | --- This type should be used to typecheck function parameters, using 27 | --- Futures.Future will not work as expected. 28 | --- 29 | local Future = {} 30 | Future.__index = Future 31 | 32 | --- @method after 33 | --- @within Future 34 | --- @param fn (Result) -> Future 35 | --- @return Future 36 | --- 37 | --- After completion, passes the Result of the current future 38 | --- to the closure, returning a new Future. 39 | function Future:after(fn) 40 | -- DEVIATION: `then` is a reserved keyword in Luau, 41 | -- this method is equivalent to FutureExt::then 42 | return self.new(function() 43 | local future = fn(self:await()) 44 | local result = future:await() 45 | 46 | if result:isOk() then 47 | return result:unwrapOk() 48 | elseif result:isErr() then 49 | error(result:unwrapErr(), 0) 50 | end 51 | end) 52 | end 53 | 54 | --- @method andThen 55 | --- @within Future 56 | --- @param fn (U...) -> Future 57 | --- @return Future 58 | --- 59 | --- After successfully resolving, create and execute another Future 60 | --- created within the closure, with the Ok result passed in the 61 | --- closure arguments, otherwise it is never executed. 62 | function Future:andThen(fn) 63 | return self.new(function() 64 | local result = self:await() 65 | 66 | if result:isOk() then 67 | local newFuture = fn(result:unwrapOk()) 68 | local newResult = newFuture:await() 69 | 70 | if newResult:isOk() then 71 | return newResult:unwrapOk() 72 | elseif newResult:isErr() then 73 | error(newResult:unwrapErr(), 0) 74 | end 75 | elseif result:isErr() then 76 | error(result:unwrapErr(), 0) 77 | end 78 | end) 79 | end 80 | 81 | --- @method await 82 | --- @within Future 83 | --- @yields 84 | --- @return Result 85 | --- 86 | --- Yields until the Future finishes execution, then returns the result. 87 | --- 88 | --- :::warning 89 | --- Because this is a yielding method, it is suggested that it is only 90 | --- used within Futures, as opposed to the main thread. \ 91 | --- \ 92 | --- See [Future:poll] for the recommended way of executing Futures. 93 | --- ::: 94 | function Future:await() 95 | local poll = self:poll() 96 | 97 | if not poll:isReady() then 98 | table.insert(self._yieldedThreads, coroutine.running()) 99 | coroutine.yield() 100 | end 101 | 102 | poll = self:poll() 103 | return poll:unwrap() 104 | end 105 | 106 | --- @method inspectErr 107 | --- @within Future 108 | --- @param fn (E) -> () 109 | --- @return Future 110 | --- 111 | --- Allows you to read the error value of a Future before passing it on. 112 | function Future:inspectErr(fn) 113 | table.insert(self._onCompletion, { 114 | on = "Err", 115 | type = "inspect", 116 | fn = fn, 117 | }) 118 | 119 | return self 120 | end 121 | 122 | --- @method inspectOk 123 | --- @within Future 124 | --- @param fn (U...) -> () 125 | --- @return Future 126 | --- 127 | --- Allows you to read the success value of a Future before passing it on. 128 | function Future:inspectOk(fn) 129 | table.insert(self._onCompletion, { 130 | on = "Ok", 131 | type = "inspect", 132 | fn = fn, 133 | }) 134 | 135 | return self 136 | end 137 | 138 | --- @method join 139 | --- @within Future 140 | --- @param otherFuture Future 141 | --- @return Future | (U..., T...) }> 142 | --- 143 | --- Joins the results of two futures into a table. 144 | --- 145 | --- Futures of different types, Err and Ok will still 146 | --- have their results joined into a table. 147 | --- 148 | --- Results of type Ok will be unwrapped in the table, 149 | --- whereas Results of type Err will not be unwrapped 150 | --- and will be added as `Result` in the table. 151 | function Future:join(otherFuture) 152 | return self.new(function() 153 | local result = self:await() 154 | local otherResult = otherFuture:await() 155 | 156 | -- unwrapOk and unwrapErr are functionally similar 157 | local results = result:isOk() and { result:unwrapOk() } or { result } 158 | 159 | if otherResult:isOk() then 160 | for _, v in { otherResult:unwrapOk() } do 161 | table.insert(results, v) 162 | end 163 | else 164 | table.insert(results, otherResult) 165 | end 166 | 167 | return results 168 | end) 169 | end 170 | 171 | --- @method joinAll 172 | --- @within Future 173 | --- @param ... Future 174 | --- @return Future | ...any }> 175 | --- 176 | --- Joins the results of two or more futures into a table. 177 | function Future:joinAll(...) 178 | local previousFuture = self 179 | 180 | for i = 1, select("#", ...) do 181 | previousFuture = self.join(previousFuture, select(i, ...)):mapOk(function(t) 182 | return table.unpack(t) 183 | end) 184 | end 185 | 186 | return previousFuture 187 | end 188 | 189 | --- @method mapErr 190 | --- @within Future 191 | --- @param fn (E) -> T 192 | --- @return Future 193 | --- 194 | --- Maps the type of the Err result of a Future. 195 | function Future:mapErr(fn) 196 | table.insert(self._onCompletion, { 197 | on = "Err", 198 | type = "map", 199 | fn = fn, 200 | }) 201 | 202 | return self 203 | end 204 | 205 | --- @method mapOk 206 | --- @within Future 207 | --- @param fn (U...) -> T... 208 | --- @return Future 209 | --- 210 | --- Maps the type of the Ok result of a Future. 211 | function Future:mapOk(fn) 212 | table.insert(self._onCompletion, { 213 | on = "Ok", 214 | type = "map", 215 | fn = fn, 216 | }) 217 | 218 | return self 219 | end 220 | 221 | --- @method orElse 222 | --- @within Future 223 | --- @param fn (E) -> Future 224 | --- @return Future 225 | --- 226 | --- On Err, executes another Future of the same type. 227 | function Future:orElse(fn) 228 | return self.new(function() 229 | local result = self:await() 230 | 231 | if result:isOk() then 232 | return result:unwrapOk() 233 | elseif result:isErr() then 234 | local newFuture = fn(result:unwrapErr()) 235 | local newResult = newFuture:await() 236 | 237 | if newResult:isOk() then 238 | return newResult:unwrapOk() 239 | elseif newResult:isErr() then 240 | error(newResult:unwrapErr(), 0) 241 | end 242 | end 243 | end) 244 | end 245 | 246 | --- @method unwrapOrElse 247 | --- @within Future 248 | --- @param fn (E) -> U... 249 | --- @return Future 250 | --- 251 | --- On Err, the result is passed to the closure to create a Ok result, 252 | --- then returns a Future with that Ok result. 253 | function Future:unwrapOrElse(fn) 254 | return self.new(function() 255 | local result = self:await() 256 | 257 | if result:isOk() then 258 | return result:unwrapOk() 259 | elseif result:isErr() then 260 | return fn(result:unwrapErr()) 261 | end 262 | end) 263 | end 264 | 265 | --- @method poll 266 | --- @within Future 267 | --- @return Poll 268 | --- 269 | --- Executes the Future on it's next resumption point, 270 | --- returning `Result::Pending` if it is not ready yet or 271 | --- `Result::Ready` if it is ready. 272 | --- 273 | --- :::danger 274 | --- Polling a Future that is ready will return the same result. 275 | --- This will however not be guaranteed behavior, and you should 276 | --- avoid polling a Future that is already ready. 277 | --- ::: 278 | function Future:poll() 279 | local threadExists = self.thread ~= nil 280 | 281 | if not threadExists then 282 | local thread = coroutine.create(function(...) 283 | local success, result = xpcall(function(...) 284 | local results = { self._callback(...) } 285 | 286 | for _, command in self._onCompletion do 287 | if command.on == "Err" then 288 | continue 289 | end 290 | 291 | if command.type == "map" then 292 | results = { command.fn(table.unpack(results)) } 293 | else 294 | command.fn(table.unpack(results)) 295 | end 296 | end 297 | 298 | return results 299 | end, function(err) 300 | for _, command in self._onCompletion do 301 | if command.on == "Ok" then 302 | continue 303 | end 304 | 305 | if command.type == "map" then 306 | _, err = pcall(command.fn, err) 307 | else 308 | command.fn(err) 309 | end 310 | end 311 | 312 | return err 313 | end, ...) 314 | 315 | if not success then 316 | result = { result } 317 | end 318 | 319 | self._threadStatus = { 320 | success = success, 321 | result = result, 322 | } 323 | 324 | for _, yieldedThread in self._yieldedThreads do 325 | if coroutine.status(yieldedThread) ~= "suspended" then 326 | continue 327 | end 328 | 329 | coroutine.resume(yieldedThread) 330 | end 331 | end) 332 | 333 | self.thread = thread 334 | coroutine.resume(thread, table.unpack(self._arguments)) 335 | end 336 | 337 | local ready = if self._threadStatus then true else false 338 | local success = if ready then self._threadStatus.success else false 339 | local result = if ready then self._threadStatus.result else {} 340 | 341 | local resultType: ("Ok" | "Err")? = if success then "Ok" elseif ready then "Err" else nil 342 | 343 | if resultType == "Ok" then 344 | return Poll.ok(table.unpack(result)) 345 | elseif resultType == "Err" then 346 | return Poll.err(table.unpack(result)) 347 | else 348 | return Poll.notReady() 349 | end 350 | end 351 | 352 | --- @function new 353 | --- @within Future 354 | --- @param callback (T...) -> U... 355 | --- @param ... T... 356 | --- @return Future 357 | --- 358 | --- Creates a new Future, taking an asynchronous callback and 359 | --- parameters to pass into that callback. 360 | function Future.new(callback, ...) 361 | return setmetatable({ 362 | _onCompletion = {}, 363 | _arguments = table.pack(...), 364 | _callback = callback, 365 | _yieldedThreads = {}, 366 | }, Future) 367 | end 368 | 369 | return Future 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luau Futures 2 | 3 | ![GitHub License](https://img.shields.io/github/license/yetanotherclown/luau-futures?style=flat-square) 4 | [![Documentation](https://img.shields.io/badge/Documentation-02B1E9?style=flat-square&logo=data:image/svg%2bxml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8dGl0bGU+TW9vbndhdmU8L3RpdGxlPgogIDxwYXRoIGQ9Ik0xMy43MzkgMTQwLjMzMWMuMjguNDkyLjQ2NSAxLjAwOC44NTEgMS40MzguODczLjk3MyAyLjE3MyAxLjAzMyAzLjI2NC4zOTcuOTg5IDIuMzc1IDMuNTg4IDIuOTgzIDUuODk5IDIuNTQ3LjUzNC0uMSAxLjkxOS0uMyAyLjEzMi0uODYyLTEuNDgzIDAtMy4xMDYuMTctNC4zNjMtLjgxOS0yLjE0Ny0xLjY4OS0uNzE1LTQuNiAxLjM4OC01LjQ3Ny0uMTM4LS40MjYtMS4xNTgtLjM5OC0xLjUzNy0uMzk2LTEuNzYyLjAwNy0yLjk0OCAxLjEwOC0zLjY2OCAyLjYyNy0uNjc1LTEuNzQ4LS4zNTQtMy4yOCAxLjE0LTQuNDg4LjM5Ny0uMzIxLjg0My0uNTM5IDEuMjg5LS43OC4xMTgtLjA2NC4zNDItLjE5Ni4xOC0uMzQxLS4xNjUtLjE0OC0uNTE2LS4xNy0uNzI1LS4yMjJhMTIuMjcyIDEyLjI3MiAwIDAgMC0yLjA4Mi0uMzJjLS44MTktLjA1OC0xLjYyNC0uMDQ0LTIuNDMuMTQ3LTMuMDcuNzI5LTQuMjc2IDMuNTg5LTUuNjM2IDYuMTAzLS41NjcgMS4wNS0xLjA1MSAyLjEyNi0xLjk5OCAyLjkwMi0xLjE1MS45NDQtMi42NzYgMS4xOC00LjExNSAxLS4zNjktLjA0Ni0uNzMtLjEyOC0xLjA5LS4yMTUtLjA4NC0uMDItLjMyNC0uMS0uMzUxLjA0My0uMDQ0LjIzNC43MzEuNDcuODk2LjU0MSAxLjA0NC40NDUgMi4xLjY4OSAzLjIyMi44MjEgMy4wNTkuMzYxIDYuNTMtMS44NTcgNy43MzQtNC42NDZ6IiBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDouMDQ5NTc0MyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEuODg1IC0xMjcuMzEpIi8+Cjwvc3ZnPg==)](https://yetanotherclown.github.io/luau-future) 5 | [![Wally Package](https://img.shields.io/badge/Wally-ad4646?style=flat-square&logoSize=auto&logo=data:image/svg%2bxml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+V2FsbHk8L3RpdGxlPjxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0yMC4zMjUgMTguMTkxYy0xLjkxNSAyLjU5OS01LjEyNyA0LjI1NC04LjM1OCA0LjE4MS0uMjk2LS41MjgtLjc2My0xLjY3My4zNDgtMS4yOTcgMi4zNTUtLjA3NiA0Ljc3LTEuMDE0IDYuMzc1LTIuNzYxLjI5OS0uODUzLjgyLS45ODcgMS4zOC0uMzAxbC4xMjcuMDkuMTI4LjA4OHpNMTIuNzg1IDYuMmMtLjg5Mi4yNjQtLjEwNCAyLjY2LjQ1OSAxLjI3Mi0uMDc1LS40MDcuMjItMS4yODgtLjQ1OC0xLjI3MnptLS41OS0uMjQyYy0uNjY0LTEuMzM1IDEuOTY2LS4zNTMgMS44ODItLjIyOC0uMzI2LS44NTYtMi4zMDItMS4yNC0yLjI2My0uMTA4bC4xNzMuMTk3ek0xMS41NCAxOS4zOGMtLjI4LTEuMzY0IDEuOTY1LS45NTggMS45My0xLjgwMS0uOTkyLS4xNi0yLjM4Mi0uODMyLTEuMzQtMS45NjMgMS4wMjctMS4wMjIgMi41MzMtMS45NTYgMi40OTItMy42NDktLjI4NS42MTItLjkyIDEuOTMtMS44MzUgMi4zODctMS41NzMgMS4wOC00LjA5IDEuMTc5LTUuMjYtLjU1LS4zNDktLjQ2My0uNjg3LTEuNDkxLS40NC0uMzQyLjQ2Ni42NjguNiAxLjcwMi0uNTYxIDEuNDUzLTEuMjQ1LS40NDEtLjM2Mi0xLjc2NC0uNC0yLjY0Ni0uNi0xLjE0NCAxLjM3Ni0uNjA4IDEuNjIzLTEuNjk0QzguNjQgOS40MyA2LjcyIDguODMgNS44NDggOC45MWMtLjk5Ni4xNjUuODUxLS40OTUgMS4xOC0uNzkuNzczLS40NTMgMS41MDYtLjk5NiAyLjA5LTEuNjgyLS41NjIuNDgyLS43NjEuNTE2LS43NDktLjI4LTEuMTUyLS41Ny0uMTM3IDEuNjkzLTEuMzk3IDEuNjY4LS45MTIuNjA1LjYxOS0xLjE0NC4yMzItMS43ODctLjIxOS0xLjIzNCAxLjUtMS4zMjIgMS40My0uMjMuNzYyLS42MjQtLjYxNi0xLjAyMy0uNjE2LTEuMTczIDEuMzQ3LTEuMzA3IDMuNDEzLTEuMzk1IDUuMTItLjg3My45MTYuMjUgMS43MDQuODYyIDIuMDA2IDEuNzg2Ljg5NCAyLjA2NC40NzMgNC4zNTEuMjc4IDYuNTA0LS4xOCAxLjExNi40OTMgMi4wNzcgMS4zODEgMi40NjYuNDI2LjkxNyAxLjkxIDEuNzUyLjU3NSAyLjYwOC0xLjUzOSAxLjQ4OC0zLjY2MyAyLjQ3Ny01LjgzOCAyLjI1MnptOS4xMjMtMS42NjVjLTEuMjctLjQ3MS0xLjc3My0xLjc0Mi0yLjg4NC0yLjM2NS0uNTMzLS42MzgtLjk2LTEuMTU0LS4yOS0xLjc4My4yOTktMS4zNjggMS43OC0xLjg1MiAyLjQ1NC0yLjk4Ljc4Ny0uOTY4LjcwNC0yLjQzMS0uMjAyLTMuMjkxLS43OTctLjg2LTIuMDc2LTEuMjA2LTIuNTI3LTIuMzg1LTEuMjMtMS4wMi0zLjAyMS0xLjA1NS00LjQ5OS0xLjY3NS0xLjMyOC0uMTk0LTIuOTA1LS4yNjEtNC4wMjEuNjA2LTEuNDkyLjAzLTEuODA3IDEuNzc3LTIuNTk0IDIuNzI2LS43My42NDktMS42NTMgMS4yNjYtMS4xNTMgMi4zMzQtMS4wNDguNzE3LjE3OCAyLjAzNi42OTIgMi43NTQuMzA3IDEuMjAyLS45OTQgMy4xNzYuOTY4IDMuNTM4Ljc4NC4wMjYgMS4xNzMtLjg2OCAxLjc5Ni0uMDQzIDEuMzc1LjIyNSAxLjA5IDEuODk4IDEuMDE4IDIuOTM2LjA4Mi45MDItMS4wMiAxLjU2NS0uMzI5IDIuNS0uMTQuODc4LS4zMDMgMS42Ni0xLjI3Ni45MjMtMy45OTktMS43MTgtNi42NDktNi4xMy02LjE2Ny0xMC40NzMuMzM0LTQuMTIyIDMuMzc3LTcuODM0IDcuMzQ1LTguOTg4IDQuMDgtMS4zMSA4Ljg0Ny4yODggMTEuMzUzIDMuNzU1IDIuNTg0IDMuNDAxIDIuNzMxIDguMzguMzE2IDExLjkxWk0xMS43NjguMDAzQzYuODQ4LjAzOSAyLjE4NSAzLjQ0NS42NTIgOC4xMmMtMS40OTUgNC4xOC0uMzU4IDkuMTEzIDIuNzc2IDEyLjI0OSAzLjI1NiAzLjQ0IDguNjMzIDQuNTY5IDEzLjAxIDIuNzc0IDQuNjM2LTEuNzg5IDcuODMtNi42OTIgNy41NDItMTEuNjYtLjE1NS00LjY2My0zLjMtOS4wNC03LjY3MS0xMC42NzJhMTEuODcyIDExLjg3MiAwIDAgMC00LjU0LS44MVoiIHN0eWxlPSJmaWxsOiNGRkY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiLz48L3N2Zz4=)](https://wally.run/package/yetanotherclown/luau-futures) 6 | [![Pesde Package](https://img.shields.io/badge/Pesde-F19D1E?style=flat-square&logo=data:image/svg%2bxml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00OS42MDI1IDBMOTIuOTAzOCAyNVY3NUw0OS42MDI1IDEwMEw2LjMwMTI3IDc1VjI1TDQ5LjYwMjUgMFpNMTQuMzAxMyAyOS42MTg4TDQ5LjYwMjUgOS4yMzc2TDg0LjkwMzggMjkuNjE4OFY3MC4zODEyTDQ5LjYwMjUgOTAuNzYyNEwzMy42MTQ4IDgxLjUzMTlWNjcuMzg0OEMzNC41MTY3IDY4LjUwNzEgMzUuNjM4OCA2OS40MjE1IDM2Ljk4MSA3MC4xMjc5QzM4Ljk3MDEgNzEuMTQ4IDQxLjAzNTcgNzEuNjU4IDQzLjE3NzkgNzEuNjU4QzQ2LjQ0MiA3MS42NTggNDkuMTQ1MiA3MC44OTI5IDUxLjI4NzMgNjkuMzYyOUM1My40ODA1IDY3Ljc4MTggNTUuMTEyNiA2NS43NjcyIDU2LjE4MzYgNjMuMzE5QzU3LjA5MTUgNjEuMzM4MiA1Ny42MzIgNTkuMjc0IDU3LjgwNTQgNTcuMTI2M0M1OS44NzIzIDU3Ljc0NTcgNjIuMjE1NyA1OC4wNTU0IDY0LjgzNTYgNTguMDU1NEM2Ny42OTE4IDU4LjA1NTQgNzAuMzY5NSA1Ny42NDczIDcyLjg2ODYgNTYuODMxM0M3NS4zNjc4IDU1Ljk2NDIgNzcuNDA3OSA1NC44MTY3IDc4Ljk4OSA1My4zODg2TDc1Ljc3NTggNDcuODAzOEM3NC41NTE3IDQ4LjkyNTggNzIuOTk2MSA0OS44NDM5IDcxLjEwOSA1MC41NTc5QzY5LjIyMTkgNTEuMjIxIDY3LjIwNzMgNTEuNTUyNSA2NS4wNjUyIDUxLjU1MjVDNjEuMzkyOSA1MS41NTI1IDU4LjY2NDMgNTAuNjg1NCA1Ni44NzkyIDQ4Ljk1MTNDNTYuNzE5NSA0OC43OTYyIDU2LjU2NyA0OC42MzY1IDU2LjQyMTcgNDguNDcyQzU1LjYxMDIgNDcuNTUzOSA1NS4wMjExIDQ2LjQ4OTYgNTQuNjU0NiA0NS4yNzkxTDU0LjY0NDMgNDUuMjQ1Mkw1NC42NjkgNDUuMjc5MUg3OS4yMTg1VjQxLjk4OTRDNzkuMjE4NSAzOS4wMzEzIDc4LjU1NTUgMzYuMzUzNiA3Ny4yMjk0IDMzLjk1NjVDNzUuOTU0MyAzMS41NTkzIDc0LjA5MjcgMjkuNjQ2NyA3MS42NDQ1IDI4LjIxODZDNjkuMjQ3NCAyNi43Mzk1IDY2LjM2NTcgMjYgNjIuOTk5NSAyNkM1OS42ODQzIDI2IDU2LjgwMjcgMjYuNzM5NSA1NC4zNTQ1IDI4LjIxODZDNTEuOTA2NCAyOS42NDY3IDUwLjAxOTMgMzEuNTU5MyA0OC42OTMyIDMzLjk1NjVDNDcuNjc0MyAzNS43OTgzIDQ3LjA0NjkgMzcuODA1NyA0Ni44MTA4IDM5Ljk3ODhDNDUuNjg4OCAzOS43MjggNDQuNDc3OCAzOS42MDI2IDQzLjE3NzkgMzkuNjAyNkM0MS4wMzU3IDM5LjYwMjYgMzguOTcwMSA0MC4xMTI3IDM2Ljk4MSA0MS4xMzI3QzM1LjMxNjIgNDEuOTY1MSAzMy45OTAyIDQzLjE1NDkgMzMuMDAyOCA0NC43MDIzVjQwLjM2NzdIMjAuNjg1NVY0Ni4yNTg1SDI1LjgxMTNWNzcuMDI2NkwxNC4zMDEzIDcwLjM4MTJWMjkuNjE4OFpNNTUuMTk2MSAzNi4wOTg2QzU0LjY1MjggMzcuMTAxNSA1NC4zMzIxIDM4LjEyMTYgNTQuMjM0IDM5LjE1ODhINzEuNzk3NkM3MS43OTc2IDM4LjAzNjcgNzEuNDQwNSAzNi45NDAxIDcwLjcyNjUgMzUuODY5MUM3MC4wNjM0IDM0Ljc0NyA2OS4wNjg5IDMzLjgwMzUgNjcuNzQyOCAzMy4wMzg0QzY2LjQ2NzcgMzIuMjczNCA2NC44ODY3IDMxLjg5MDggNjIuOTk5NSAzMS44OTA4QzYxLjExMjQgMzEuODkwOCA1OS41MDU4IDMyLjI5ODkgNTguMTc5OCAzMy4xMTQ5QzU2LjkwNDcgMzMuODggNTUuOTEwMSAzNC44NzQ1IDU1LjE5NjEgMzYuMDk4NlpNNDkuNjQ1MSA1MS41NjkyQzQ5LjMwNzYgNTAuNjY0MSA0OC44MzgxIDQ5Ljg3MSA0OC4yMzY3IDQ5LjE4OThDNDguMDg4NSA0OS4wMjE5IDQ3LjkzMjMgNDguODYwOSA0Ny43NjgxIDQ4LjcwNjdDNDYuMDg1IDQ3LjA3NDYgNDQuMDQ0OSA0Ni4yNTg1IDQxLjY0NzggNDYuMjU4NUM0MC4xMTc3IDQ2LjI1ODUgMzguNjEzMSA0Ni41NjQ1IDM3LjEzNCA0Ny4xNzY2QzM1Ljg1OTQgNDcuNjc3MyAzNC42ODYzIDQ4LjU0MzggMzMuNjE0OCA0OS43NzU5VjYxLjQ3QzM0LjY4NjMgNjIuNjY2NCAzNS44NTk0IDYzLjUzNzggMzcuMTM0IDY0LjA4NEMzOC42MTMxIDY0LjY5NjEgNDAuMTE3NyA2NS4wMDIxIDQxLjY0NzggNjUuMDAyMUM0NC4wNDQ5IDY1LjAwMjEgNDYuMDg1IDY0LjE4NjEgNDcuNzY4MSA2Mi41NTRDNDkuNDUxMiA2MC45MjE5IDUwLjI5MjggNTguNjAxMiA1MC4yOTI4IDU1LjU5MjFDNTAuMjkyOCA1NC4wNjc5IDUwLjA3NjkgNTIuNzI3IDQ5LjY0NTEgNTEuNTY5MloiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KPC9zdmc+)](https://pesde.daimond113.com/packages/yetanotherclown/luau_futures) 7 | 8 | Futures represent a read-only asynchronous value, one that may not have 9 | finished computation like. 10 | 11 | This design is inspired by the Futures crate in Rust. 12 | 13 | > [!IMPORTANT] 14 | > After almost two years of being the oldest Futures implementation on Wally, 15 | > Luau Futures v2.0.0 has released, with several key changes. 16 | > 17 | > Importantly, the Wally scope has been changed to `yetanotherclown/luau-futures`. 18 | > If you are still using the v1.x.x Future library make sure to update your `wally.toml` to upgrade. 19 | > 20 | > You can find out more [here](https://github.com/YetAnotherClown/luau-futures/releases/latest). 21 | 22 | ## Basic Use 23 | 24 | Creating a future is very simple: 25 | 26 | ```luau 27 | local Futures = require("@packages/Futures") 28 | local Future = Futures.Future 29 | 30 | local myFuture = Future.new(function() 31 | yield() 32 | return 1, 2, 3 33 | end) 34 | 35 | ``` 36 | 37 | When you create a future, it wont begin execution until it is either polled or awaited. 38 | 39 | Polling will advance the future to it's next resumption point every time that it is called, returning a [Poll](https://yetanotherclown.github.io/luau-futures/api/Poll) to let you check the status of the future. 40 | 41 | If the Poll is ready, you can also unwrap it to get the [Result](https://yetanotherclown.github.io/luau-futures/api/Result) 42 | 43 | ```luau 44 | local poll = myFuture:poll() 45 | if poll:isReady() then 46 | local result = poll:unwrap() 47 | -- Handle result 48 | end 49 | ``` 50 | 51 | Awaiting a future will yield the current thread until the future finishes execution. As such, it is recommended that you only use the await method within other futures, preferring to use poll instead. 52 | 53 | ```luau 54 | local result = myFuture:await() 55 | -- Handle result 56 | ``` 57 | 58 | To read the result, you can use [Result:isOk](https://yetanotherclown.github.io/luau-futures/api/Result#isOk) or [Result:isErr](https://yetanotherclown.github.io/luau-futures/api/Result#isOk) to check what type the Result is. 59 | 60 | You can then use [Result:unwrapOk](https://yetanotherclown.github.io/luau-futures/api/Result#unwrapOk) or [Result:unwrapErr](https://yetanotherclown.github.io/luau-futures/api/Result#unwrapErr) to get the value of the result. 61 | 62 | ```luau 63 | if result:isOk() then 64 | print(result:unwrapOk()) -- 1, 2, 3 65 | elseif result:isErr() then 66 | warn(result:unwrapErr()) -- An error occurred 67 | end 68 | ``` 69 | 70 | There are also several other methods for chaining, combining, and mapping futures, as well as other utilities for working with futures. 71 | 72 | It is suggested to read the [API Documentation](https://yetanotherclown.github.io/luau-futures) for more information about these methods. 73 | 74 | ## Why Luau Futures 75 | 76 | ### Laziness 77 | 78 | Like in Rust, Luau Future is lazy. Unlike Promises which are eager. 79 | 80 | Futures will not begin execution until polled or awaited, where as in Promises, execution is begun immediately or scheduled to be done as soon as it can. 81 | 82 | Polling will execute until the next suspension point, until execution is finished. By awaiting a Future, it will yield the current thread until execution has completed. 83 | 84 | ### Strictly Typed 85 | 86 | Strict Typing is a feature, with API designed to work with the Luau type solver. 87 | 88 | There are currently some restrictions, see below for more information. 89 | 90 | ### Functional 91 | 92 | The API is designed to be functional, taking inspiration from the Rust futures crate. 93 | 94 | ## Why you Shouldn't Use Luau Futures 95 | 96 | ### Futures are Lazy 97 | 98 | Sometimes, you might not want the Laziness of Futures, and instead want execution to begin when it can. Promises begin execution as soon as they're made, 99 | allowing the result to be completed much sooner than with a Future. Futures are lazy by design, you might find that you want this laziness for a certain 100 | purpose and that is fine, but sometimes you might not. 101 | 102 | ### Promises Just Work 103 | 104 | roblox-lua-promise works, and has worked for some time now. Do you need a battle tested strategy for asynchronous programming? Use roblox-lua-promise! 105 | If you're already using Promises, keep using them. 106 | 107 | ### Promises are more Common 108 | 109 | Working on a library? Introducing new developers to your team? It would be easier for them to understand Promises, as they're already widely popular in 110 | the JavaScript ecosystem as well as in Luau. 111 | 112 | ## A Note on Typechecking 113 | 114 | The following typechecking restrictions should be resolved in the Luau Solver V2, in which recursive type restrictions 115 | should be loosened. 116 | 117 | ### Exported Types 118 | 119 | The Futures library exports two types because of these restrictions. `FutureLike` should be used when your being given a future, such as in a function with a future as a parameter. The `Future` type should be used when returning a future, such as in a function return. 120 | 121 | ```luau 122 | function Class:method(future: Futures.FutureLike): Futures.Future 123 | return future:andThen(function(...) 124 | -- ... 125 | end) :: any 126 | end 127 | ``` 128 | 129 | > [!NOTE] 130 | > To avoid recursive type restrictions, there are internally multiple types like FutureFirst, FutureNext, FutureLast and FutureExhausted. 131 | > 132 | > The Futures.Future type is just FutureFirst, so when you use that type it will expect a FutureFirst which is the first type you get when creating a future with Future.new(). 133 | > 134 | > If you are chaining a future in a function that returns one, you can annotate the return type to be Futures.Future and then typecast the returned future with :: any like in the example. 135 | 136 | ### Recursive Types 137 | 138 | Some methods, such as `andThen`, `mapOk`, `mapErr`, etc. will return a recursive type with different parameters. 139 | Currently, there are restrictions in place in the Luau type solver to prevent this. The Futures library has a workaround 140 | to allow you to chain up to 3 of these methods. When you hit the limit, you will be returned a generic 141 | Future that is typed as `Future`. 142 | 143 | ### Join Methods 144 | 145 | The Join methods currently will always return a generic Future. Currently, it is impossible to type these methods. 146 | 147 | ### UnwrapOrElse 148 | [Future:unwrapOrElse](https://yetanotherclown.github.io/luau-future/api/Future#unwrapOrElse) should return the type `Future`. However, due to recursive type restrictions, it will return `Future`. 149 | -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "jsdotlua/boolean" 7 | version = "1.2.7" 8 | dependencies = [["number", "jsdotlua/number@1.2.7"]] 9 | 10 | [[package]] 11 | name = "jsdotlua/chalk" 12 | version = "0.2.1" 13 | dependencies = [] 14 | 15 | [[package]] 16 | name = "jsdotlua/collections" 17 | version = "1.2.7" 18 | dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"]] 19 | 20 | [[package]] 21 | name = "jsdotlua/console" 22 | version = "1.2.7" 23 | dependencies = [["collections", "jsdotlua/collections@1.2.7"]] 24 | 25 | [[package]] 26 | name = "jsdotlua/diff-sequences" 27 | version = "3.6.1-rc.2" 28 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 29 | 30 | [[package]] 31 | name = "jsdotlua/emittery" 32 | version = "3.6.1-rc.2" 33 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"]] 34 | 35 | [[package]] 36 | name = "jsdotlua/es7-types" 37 | version = "1.2.7" 38 | dependencies = [] 39 | 40 | [[package]] 41 | name = "jsdotlua/expect" 42 | version = "3.6.1-rc.2" 43 | dependencies = [["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-matcher-utils", "jsdotlua/jest-matcher-utils@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-snapshot", "jsdotlua/jest-snapshot@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["promise", "jsdotlua/promise@3.5.2"]] 44 | 45 | [[package]] 46 | name = "jsdotlua/instance-of" 47 | version = "1.2.7" 48 | dependencies = [] 49 | 50 | [[package]] 51 | name = "jsdotlua/jest" 52 | version = "3.6.1-rc.2" 53 | dependencies = [["jest-core", "jsdotlua/jest-core@3.6.1-rc.2"]] 54 | 55 | [[package]] 56 | name = "jsdotlua/jest-circus" 57 | version = "3.6.1-rc.2" 58 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["expect", "jsdotlua/expect@3.6.1-rc.2"], ["jest-each", "jsdotlua/jest-each@3.6.1-rc.2"], ["jest-environment", "jsdotlua/jest-environment@3.6.1-rc.2"], ["jest-matcher-utils", "jsdotlua/jest-matcher-utils@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-runtime", "jsdotlua/jest-runtime@3.6.1-rc.2"], ["jest-snapshot", "jsdotlua/jest-snapshot@3.6.1-rc.2"], ["jest-test-result", "jsdotlua/jest-test-result@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"], ["promise", "jsdotlua/promise@3.5.2"], ["throat", "jsdotlua/throat@3.6.1-rc.2"]] 59 | 60 | [[package]] 61 | name = "jsdotlua/jest-config" 62 | version = "3.6.1-rc.2" 63 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-each", "jsdotlua/jest-each@3.6.1-rc.2"], ["jest-environment-roblox", "jsdotlua/jest-environment-roblox@3.6.1-rc.2"], ["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["jest-validate", "jsdotlua/jest-validate@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["promise", "jsdotlua/promise@3.5.2"]] 64 | 65 | [[package]] 66 | name = "jsdotlua/jest-console" 67 | version = "3.6.1-rc.2" 68 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-each", "jsdotlua/jest-each@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 69 | 70 | [[package]] 71 | name = "jsdotlua/jest-core" 72 | version = "3.6.1-rc.2" 73 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["emittery", "jsdotlua/emittery@3.6.1-rc.2"], ["jest-config", "jsdotlua/jest-config@3.6.1-rc.2"], ["jest-console", "jsdotlua/jest-console@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-reporters", "jsdotlua/jest-reporters@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-runner", "jsdotlua/jest-runner@3.6.1-rc.2"], ["jest-runtime", "jsdotlua/jest-runtime@3.6.1-rc.2"], ["jest-snapshot", "jsdotlua/jest-snapshot@3.6.1-rc.2"], ["jest-test-result", "jsdotlua/jest-test-result@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"], ["promise", "jsdotlua/promise@3.5.2"]] 74 | 75 | [[package]] 76 | name = "jsdotlua/jest-diff" 77 | version = "3.6.1-rc.2" 78 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["diff-sequences", "jsdotlua/diff-sequences@3.6.1-rc.2"], ["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"]] 79 | 80 | [[package]] 81 | name = "jsdotlua/jest-each" 82 | version = "3.6.1-rc.2" 83 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"]] 84 | 85 | [[package]] 86 | name = "jsdotlua/jest-environment" 87 | version = "3.6.1-rc.2" 88 | dependencies = [["jest-fake-timers", "jsdotlua/jest-fake-timers@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 89 | 90 | [[package]] 91 | name = "jsdotlua/jest-environment-roblox" 92 | version = "3.6.1-rc.2" 93 | dependencies = [["jest-environment", "jsdotlua/jest-environment@3.6.1-rc.2"], ["jest-fake-timers", "jsdotlua/jest-fake-timers@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"]] 94 | 95 | [[package]] 96 | name = "jsdotlua/jest-fake-timers" 97 | version = "3.6.1-rc.2" 98 | dependencies = [["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 99 | 100 | [[package]] 101 | name = "jsdotlua/jest-get-type" 102 | version = "3.6.1-rc.2" 103 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"]] 104 | 105 | [[package]] 106 | name = "jsdotlua/jest-globals" 107 | version = "3.6.1-rc.2" 108 | dependencies = [["expect", "jsdotlua/expect@3.6.1-rc.2"], ["jest-environment", "jsdotlua/jest-environment@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 109 | 110 | [[package]] 111 | name = "jsdotlua/jest-matcher-utils" 112 | version = "3.6.1-rc.2" 113 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-diff", "jsdotlua/jest-diff@3.6.1-rc.2"], ["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"]] 114 | 115 | [[package]] 116 | name = "jsdotlua/jest-message-util" 117 | version = "3.6.1-rc.2" 118 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"]] 119 | 120 | [[package]] 121 | name = "jsdotlua/jest-mock" 122 | version = "3.6.1-rc.2" 123 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 124 | 125 | [[package]] 126 | name = "jsdotlua/jest-reporters" 127 | version = "3.6.1-rc.2" 128 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-console", "jsdotlua/jest-console@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-test-result", "jsdotlua/jest-test-result@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["path", "jsdotlua/path@3.6.1-rc.2"]] 129 | 130 | [[package]] 131 | name = "jsdotlua/jest-roblox-shared" 132 | version = "3.6.1-rc.2" 133 | dependencies = [["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 134 | 135 | [[package]] 136 | name = "jsdotlua/jest-runner" 137 | version = "3.6.1-rc.2" 138 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["emittery", "jsdotlua/emittery@3.6.1-rc.2"], ["jest-circus", "jsdotlua/jest-circus@3.6.1-rc.2"], ["jest-console", "jsdotlua/jest-console@3.6.1-rc.2"], ["jest-environment", "jsdotlua/jest-environment@3.6.1-rc.2"], ["jest-message-util", "jsdotlua/jest-message-util@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-runtime", "jsdotlua/jest-runtime@3.6.1-rc.2"], ["jest-test-result", "jsdotlua/jest-test-result@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["jest-util", "jsdotlua/jest-util@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"], ["promise", "jsdotlua/promise@3.5.2"], ["throat", "jsdotlua/throat@3.6.1-rc.2"]] 139 | 140 | [[package]] 141 | name = "jsdotlua/jest-runtime" 142 | version = "3.6.1-rc.2" 143 | dependencies = [["emittery", "jsdotlua/emittery@3.6.1-rc.2"], ["expect", "jsdotlua/expect@3.6.1-rc.2"], ["jest-fake-timers", "jsdotlua/jest-fake-timers@3.6.1-rc.2"], ["jest-mock", "jsdotlua/jest-mock@3.6.1-rc.2"], ["jest-snapshot", "jsdotlua/jest-snapshot@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"]] 144 | 145 | [[package]] 146 | name = "jsdotlua/jest-snapshot" 147 | version = "3.6.1-rc.2" 148 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-diff", "jsdotlua/jest-diff@3.6.1-rc.2"], ["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-matcher-utils", "jsdotlua/jest-matcher-utils@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["pretty-format", "jsdotlua/pretty-format@3.6.1-rc.2"], ["promise", "jsdotlua/promise@3.5.2"]] 149 | 150 | [[package]] 151 | name = "jsdotlua/jest-test-result" 152 | version = "3.6.1-rc.2" 153 | dependencies = [["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 154 | 155 | [[package]] 156 | name = "jsdotlua/jest-types" 157 | version = "3.6.1-rc.2" 158 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"]] 159 | 160 | [[package]] 161 | name = "jsdotlua/jest-util" 162 | version = "3.6.1-rc.2" 163 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["picomatch", "jsdotlua/picomatch@0.4.0"], ["promise", "jsdotlua/promise@3.5.2"]] 164 | 165 | [[package]] 166 | name = "jsdotlua/jest-validate" 167 | version = "3.6.1-rc.2" 168 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 169 | 170 | [[package]] 171 | name = "jsdotlua/luau-polyfill" 172 | version = "1.2.7" 173 | dependencies = [["boolean", "jsdotlua/boolean@1.2.7"], ["collections", "jsdotlua/collections@1.2.7"], ["console", "jsdotlua/console@1.2.7"], ["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"], ["math", "jsdotlua/math@1.2.7"], ["number", "jsdotlua/number@1.2.7"], ["string", "jsdotlua/string@1.2.7"], ["symbol-luau", "jsdotlua/symbol-luau@1.0.1"], ["timers", "jsdotlua/timers@1.2.7"]] 174 | 175 | [[package]] 176 | name = "jsdotlua/luau-regexp" 177 | version = "0.2.1" 178 | dependencies = [] 179 | 180 | [[package]] 181 | name = "jsdotlua/math" 182 | version = "1.2.7" 183 | dependencies = [] 184 | 185 | [[package]] 186 | name = "jsdotlua/number" 187 | version = "1.2.7" 188 | dependencies = [] 189 | 190 | [[package]] 191 | name = "jsdotlua/path" 192 | version = "3.6.1-rc.2" 193 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 194 | 195 | [[package]] 196 | name = "jsdotlua/picomatch" 197 | version = "0.4.0" 198 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["promise", "jsdotlua/promise@3.5.2"]] 199 | 200 | [[package]] 201 | name = "jsdotlua/pretty-format" 202 | version = "3.6.1-rc.2" 203 | dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["jest-get-type", "jsdotlua/jest-get-type@3.6.1-rc.2"], ["jest-roblox-shared", "jsdotlua/jest-roblox-shared@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["react-is", "jsdotlua/react-is@17.2.0"]] 204 | 205 | [[package]] 206 | name = "jsdotlua/promise" 207 | version = "3.5.2" 208 | dependencies = [] 209 | 210 | [[package]] 211 | name = "jsdotlua/react-is" 212 | version = "17.2.0" 213 | dependencies = [["shared", "jsdotlua/shared@17.2.0"]] 214 | 215 | [[package]] 216 | name = "jsdotlua/shared" 217 | version = "17.2.0" 218 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] 219 | 220 | [[package]] 221 | name = "jsdotlua/string" 222 | version = "1.2.7" 223 | dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["number", "jsdotlua/number@1.2.7"]] 224 | 225 | [[package]] 226 | name = "jsdotlua/symbol-luau" 227 | version = "1.0.1" 228 | dependencies = [] 229 | 230 | [[package]] 231 | name = "jsdotlua/throat" 232 | version = "3.6.1-rc.2" 233 | dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"]] 234 | 235 | [[package]] 236 | name = "jsdotlua/timers" 237 | version = "1.2.7" 238 | dependencies = [["collections", "jsdotlua/collections@1.2.7"]] 239 | 240 | [[package]] 241 | name = "yetanotherclown/luau-futures" 242 | version = "2.0.0-beta.1" 243 | dependencies = [["Jest", "jsdotlua/jest@3.6.1-rc.2"], ["JestGlobals", "jsdotlua/jest-globals@3.6.1-rc.2"]] 244 | --------------------------------------------------------------------------------