From 8c4bd3b9a7993b5581c7ad175049b2b0f3a85ec6 Mon Sep 17 00:00:00 2001 From: Clown Date: Fri, 29 Nov 2024 00:22:25 -0500 Subject: [PATCH] Luau Futures v2.0.0 (#3) --- .darklua.json | 24 ++ .gitignore | 11 +- .luaurc | 3 + .moonwave/custom.css | 94 +++++++ .vscode/settings.json | 16 ++ CONTRIBUTING.md | 20 ++ README.md | 211 ++++++++------- aftman.toml | 8 - build.project.json | 13 +- default.project.json | 26 +- dev.project.json | 22 ++ docs/installation.md | 40 +++ docs/intro.mdx | 66 +++++ docs/typechecking.md | 66 +++++ lib/Output.lua | 52 ---- lib/Result.lua | 94 ------- lib/init.lua | 129 --------- moonwave.toml | 30 +-- pages/index.js | 113 ++++++++ pages/index.module.css | 0 pesde.lock | 3 + pesde.toml | 21 ++ rokit.toml | 12 + scripts/build.sh | 13 + scripts/dev.sh | 13 + scripts/install-packages.sh | 6 + scripts/publish-pesde.sh | 22 ++ scripts/publish-wally.sh | 22 ++ scripts/run-tests.server.luau | 19 ++ scripts/test.sh | 16 ++ selene.toml | 5 +- selene_definitions.yml | 7 + src/Future.luau | 375 +++++++++++++++++++++++++++ src/Poll.luau | 143 ++++++++++ src/Result.luau | 114 ++++++++ src/__tests__/after.test.luau | 43 +++ src/__tests__/andThen.test.luau | 26 ++ src/__tests__/await.test.luau | 37 +++ src/__tests__/chaining.test.luau | 45 ++++ src/__tests__/inspectError.test.luau | 23 ++ src/__tests__/inspectOk.test.luau | 26 ++ src/__tests__/join.test.luau | 45 ++++ src/__tests__/joinAll.test.luau | 38 +++ src/__tests__/mapError.test.luau | 47 ++++ src/__tests__/mapOk.test.luau | 24 ++ src/__tests__/orElse.test.luau | 28 ++ src/__tests__/ordering.test.luau | 59 +++++ src/__tests__/poll.test.luau | 49 ++++ src/__tests__/unwrapOrElse.test.luau | 26 ++ src/init.luau | 146 +++++++++++ src/jest.config.luau | 9 + src/utils.luau | 112 ++++++++ stylua.toml | 1 + wally.lock | 243 +++++++++++++++++ wally.toml | 27 +- 55 files changed, 2457 insertions(+), 426 deletions(-) create mode 100644 .darklua.json create mode 100644 .luaurc create mode 100644 .moonwave/custom.css create mode 100644 .vscode/settings.json create mode 100644 CONTRIBUTING.md delete mode 100644 aftman.toml create mode 100644 dev.project.json create mode 100644 docs/installation.md create mode 100644 docs/intro.mdx create mode 100644 docs/typechecking.md delete mode 100644 lib/Output.lua delete mode 100644 lib/Result.lua delete mode 100644 lib/init.lua create mode 100644 pages/index.js create mode 100644 pages/index.module.css create mode 100644 pesde.lock create mode 100644 pesde.toml create mode 100644 rokit.toml create mode 100644 scripts/build.sh create mode 100644 scripts/dev.sh create mode 100644 scripts/install-packages.sh create mode 100644 scripts/publish-pesde.sh create mode 100644 scripts/publish-wally.sh create mode 100644 scripts/run-tests.server.luau create mode 100644 scripts/test.sh create mode 100644 selene_definitions.yml create mode 100644 src/Future.luau create mode 100644 src/Poll.luau create mode 100644 src/Result.luau create mode 100644 src/__tests__/after.test.luau create mode 100644 src/__tests__/andThen.test.luau create mode 100644 src/__tests__/await.test.luau create mode 100644 src/__tests__/chaining.test.luau create mode 100644 src/__tests__/inspectError.test.luau create mode 100644 src/__tests__/inspectOk.test.luau create mode 100644 src/__tests__/join.test.luau create mode 100644 src/__tests__/joinAll.test.luau create mode 100644 src/__tests__/mapError.test.luau create mode 100644 src/__tests__/mapOk.test.luau create mode 100644 src/__tests__/orElse.test.luau create mode 100644 src/__tests__/ordering.test.luau create mode 100644 src/__tests__/poll.test.luau create mode 100644 src/__tests__/unwrapOrElse.test.luau create mode 100644 src/init.luau create mode 100644 src/jest.config.luau create mode 100644 src/utils.luau create mode 100644 stylua.toml create mode 100644 wally.lock diff --git a/.darklua.json b/.darklua.json new file mode 100644 index 0000000..b5195e9 --- /dev/null +++ b/.darklua.json @@ -0,0 +1,24 @@ +{ + "process": [ + { + "rule": "convert_require", + "current": { + "name": "path", + "sources": { + "@Project": "src/", + "@DevPackages": "DevPackages/" + } + }, + "target": { + "name": "roblox", + "rojo_sourcemap": "sourcemap.json", + "indexing_style": "wait_for_child" + } + }, + { + "rule": "inject_global_value", + "identifier": "NOCOLOR", + "env": "NOCOLOR" + } + ] +} diff --git a/.gitignore b/.gitignore index 6251bf8..2276ce9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,9 @@ -/.vscode -/build -/Packages - /*.rbxlx.lock /*.rbxl.lock +/*.rbxl /*.rbxm -sourcemap.json -wally.lock \ No newline at end of file +DevPackages/ +dist/ + +sourcemap.json \ No newline at end of file diff --git a/.luaurc b/.luaurc new file mode 100644 index 0000000..e2b625c --- /dev/null +++ b/.luaurc @@ -0,0 +1,3 @@ +{ + "languageMode": "strict" +} diff --git a/.moonwave/custom.css b/.moonwave/custom.css new file mode 100644 index 0000000..77dcb32 --- /dev/null +++ b/.moonwave/custom.css @@ -0,0 +1,94 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +:root { + --ifm-font-family-base: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --ifm-navbar-background-color: var(--ifm-background-color); + --ifm-navbar-search-input-background-color: var(--ifm-background-color); + --ifm-navbar-shadow: none; +} + +:root[data-theme="light"] { + --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"); +} + +:root[data-theme="dark"] { + --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"); +} + +.navbar__search { + margin-left: 0%; + margin-right: 12px; +} + +.navbar__search-input { + width: 0px; + transition: width 500ms, padding 500ms; + outline: none; + background-color: var(--ifm-navbar-search-input-background-color); + background-image: var(--ifm-navbar-search-input-icon); + background-repeat: no-repeat; + background-position: 0% 50%; + background-size: 24px; + padding: 0 24px 0 0; +} + +.navbar__search-input:hover, +.navbar__search-input:focus { + width: 172px; + padding: 0 8px 0 36px; +} + +.discord-logo-link { + background-color: var(--ifm-navbar-link-color); + transition: background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default); + 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"); + mask-repeat: no-repeat; + mask-position: center; + display: flex; + width: 22px; + height: 22px; + margin: 0 6px 0 6px; + order: 5; +} + +.discord-logo-link:hover { + background-color: var(--ifm-navbar-link-hover-color); +} + +.navbar__link[href*="https://github.com/"] +{ + background-color: var(--ifm-navbar-link-color); + transition: background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default); + 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"); + mask-repeat: no-repeat; + mask-position: center; + display: flex; + width: 20px; + height: 20px; + margin: 0 6px 0 18px; + order: 4; +} + +.navbar__link[href*="https://github.com/"]:hover +{ + background-color: var(--ifm-navbar-link-hover-color); +} + +.footer { + --ifm-footer-background-color: var(--ifm-background-color); + --ifm-footer-color: var(--ifm-color-content-secondary); +} + +div[class*="docSidebarContainer"], +aside[class*="docSidebarContainer"] { + border: 0; +} + +.hero-button:not(:hover) { + color: var(--ifm-color-secondary) !important; +} + +div[class*="sourceButtonText"] { + border: 0px; +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fefbc8e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.formatOnSave": true, + "luau-lsp.completion.imports.enabled": true, + "luau-lsp.completion.imports.suggestServices": true, + "luau-lsp.completion.imports.suggestRequires": false, + "luau-lsp.require.mode": "relativeToFile", + "luau-lsp.require.directoryAliases": { + "@Project": "src/", + "@DevPackages": "DevPackages/" + }, + "luau-lsp.ignoreGlobs": [ + "DevPackages/*", + "dist/*", + ], + "stylua.targetReleaseVersion": "latest" +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f60bb6c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing + +## Running Tests + +This example has [run-in-roblox](https://github.com/rojo-rbx/run-in-roblox) setup to allow you to run tests from the CLI. +To do so, run the `scripts/test.sh` script and it will open up studio and run your tests. + +If you do not wish to use `run-in-roblox`, you can serve the project with Rojo by running the `scripts/dev.sh`. +Your tests will run and output the results when you run the server in Studio. + +## Project Structure + +You can find our `run-tests.luau` script in the `scripts` folder. +This is where we define our runCLI Options and our project directories for Jest. + +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. + +The rest of the project has been setup for use with Darklua and String Requires, and provides scripts to make it simple to use. +The structure is based on [roblox-project-template](https://github.com/grilme99/roblox-project-template), +which provides a setup for a Roblox experience with Darklua and more. \ No newline at end of file diff --git a/README.md b/README.md index af9f27b..fb386ed 100644 --- a/README.md +++ b/README.md @@ -1,127 +1,148 @@ -# Future -**[View Docs](https://yetanotherclown.github.io/Luau-Future/)** +# Luau Futures -> A Minimal, Typed Future Implementation inspired by the concept of Futures from the Rust Ecosystem. +![GitHub License](https://img.shields.io/github/license/yetanotherclown/luau-futures?style=flat-square) +[![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) +[![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) +[![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) +Futures represent a read-only asynchronous value, one that may not have +finished computation like. -## Luau Futures +This design is inspired by the Futures crate in Rust. -Futures are a Data-Driven approach to asynchronous calls, what this means is that Futures -represent a value that does not exist quite yet, similar to Promises. +> [!IMPORTANT] +> After almost two years of being the oldest Futures implementation on Wally, +> Luau Futures v2.0.0 has released, with several key changes. +> +> Importantly, the Wally scope has been changed to `yetanotherclown/luau-futures`. +> If you are still using the v1.x.x Future library make sure to update your `wally.toml` to upgrade. +> +> You can find out more [here](https://github.com/YetAnotherClown/luau-futures/releases/tag/v2.0.0). -Unlike Promises, Futures take on a Data-Driven approach as opposed to a Event-Driven approach. -Futures have no events for you to react to, there is no `andThen` or any other event-like methods -or functions for Futures. +## Basic Use -In order to use Futures, you must do something called polling, you can call the `isReady` method to see if the -future has a result ready, and then you can call the `output` method to receive a result. -The future can either be ok or an error, you can use the `ok` and `error` methods respectively to check. -To get the value `T` or `Error` you can call the `unwrap` method on the result. +Creating a future is very simple: ---- +```luau +local Futures = require("@packages/Futures") +local Future = Futures.Future -### Why use this? +local myFuture = Future.new(function() + yield() + return 1, 2, 3 +end) -Don't. Use Promises. You shouldn't need to use this and shouldn't unless it fits a certain use case. -Promises have Chaining, Joining, Cancellation, and many more features that Futures don't have. +``` -Futures are simply a lightweight alternative to Promises that uses long polling. Any gains you may get from -using Futures will be so insignificant you'd only be hurting yourself by using them in most cases. +When you create a future, it wont begin execution until it is either polled or awaited. -> See [Roblox Lua Promise](https://eryn.io/roblox-lua-promise/) -> and [Why Use Promises?](https://eryn.io/roblox-lua-promise/docs/WhyUsePromises) by Evaera +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-future/api/Poll) to let you check the status of the future. +If the Poll is ready, you can also unwrap it to get the [Result](https://yetanotherclown.github.io/luau-future/api/Result) -### Why I use Luau Futures +```luau +local poll = myFuture:poll() +if poll:isReady() then + local result = poll:unwrap() + -- Handle result +end +``` -When writing code that ran every frame I found myself needing to represent yielding asynchronous calls in a way where -I could store the future value and use it in a future frame. Promises felt like a good first step, but it just felt like -they did not fit the Data-Driven architecture I was going for, with Promises being Event-Driven. +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. -So here comes Luau Futures, a Data-Driven Approach to handling asynchronous code, -built for a library such as [Matter](https://github.com/evaera/matter), which had no built-in method for handling Asynchronous Calls. +```luau +local result = myFuture:await() +-- Handle result +``` ---- +To read the result, you can use [Result:isOk](https://yetanotherclown.github.io/luau-future/api/Result#isOk) or [Result:isErr](https://yetanotherclown.github.io/luau-future/api/Result#isOk) to check what type the Result is. -### Basic Usage +You can then use [Result:unwrapOk](https://yetanotherclown.github.io/luau-future/api/Result#unwrapOk) or [Result:unwrapErr](https://yetanotherclown.github.io/luau-future/api/Result#unwrapErr) to get the value of the result. -```lua -local Future = require(path.to.module) +```luau +if result:isOk() then + print(result:unwrapOk()) -- 1, 2, 3 +elseif result:isErr() then + warn(result:unwrapErr()) -- An error occurred +end +``` --- Create a future -local myFuture = Future.new(function(...) - -- Something that yields -end, ...) +There are also several other methods for chaining, combining, and mapping futures, as well as other utilities for working with futures. --- Poll the Future to see if it is ready. -if future:isReady() then - local result = myFuture:output() +It is suggested to read the [API Documentation](https://yetanotherclown.github.io/luau-future) for more information about these methods. - if result:ok() then - local returnedValues = result:unwrap() - -- Do something - elseif result:error() then - warn(result:unwrap()) - end +## Why Luau Futures -elseif myFuture:isPending() then - -- Poll the Future to see if it is still pending. +### Laziness - warn("Future is still pending!") -end -``` +Like in Rust, Luau Future is lazy. Unlike Promises which are eager. -In a [Matter](https://github.com/evaera/matter) System: -```lua --- Basic concept of Futures in a Matter System -local function exampleSystem(world) - - -- Create Futures - for id in world:query():without(FutureComponent) do - world:insert(id, FutureComponent { - future = Future.new(function() - -- Something that yields - end) - }) - end - - -- Poll Futures - for id, future in world:query(FutureComponent) do - local future = future.future - - if future:isReady() then - local result = myFuture:output() - - if result:ok() then - local returnedValues = result:unwrap() - -- Do something - elseif result:error() then - warn(result:unwrap()) - end - - world:remove(id, FutureComponent) - end - end -end -``` +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. ---- +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. -### Installing with Wally +### Strictly Typed -```toml -[dependencies] -Future = "yetanotherclown/future@1.1.0" -``` +Strict Typing is a feature, with API designed to work with the Luau type solver. + +There are currently some restrictions, see below for more information. + +### Functional + +The API is designed to be functional, taking inspiration from the Rust futures crate. + +## Why you Shouldn't Use Luau Futures -Note: Wally does not export types automatically and will display a type-error in one of the Dependencies. -To fix this, see https://github.com/JohnnyMorganz/wally-package-types. +### Futures are Lazy -### Building with Rojo +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, +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 +purpose and that is fine, but sometimes you might not. -To build yourself, use: -```bash -rojo build -o "Future.rbxm" +### Promises Just Work + +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! +If you're already using Promises, keep using them. + +### Promises are more Common + +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 +the JavaScript ecosystem as well as in Luau. + +## A Note on Typechecking + +The following typechecking restrictions should be resolved in the Luau Solver V2, in which recursive type restrictions +should be loosened. + +### Exported Types + +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. + +```luau +function Class:method(future: Futures.FutureLike): Futures.Future + return future:andThen(function(...) + -- ... + end) :: any +end ``` -For more help, check out [the Rojo documentation](https://rojo.space/docs). \ No newline at end of file +> [!NOTE] +> To avoid recursive type restrictions, there are internally multiple types like FutureFirst, FutureNext, FutureLast and FutureExhausted. +> +> 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(). +> +> 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. + +### Recursive Types + +Some methods, such as `andThen`, `mapOk`, `mapErr`, etc. will return a recursive type with different parameters. +Currently, there are restrictions in place in the Luau type solver to prevent this. The Futures library has a workaround +to allow you to chain up to 3 of these methods. When you hit the limit, you will be returned a generic +Future that is typed as `Future`. + +### Join Methods + +The Join methods currently will always return a generic Future. Currently, it is impossible to type these methods. + +### UnwrapOrElse +[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`. diff --git a/aftman.toml b/aftman.toml deleted file mode 100644 index 975e89d..0000000 --- a/aftman.toml +++ /dev/null @@ -1,8 +0,0 @@ -# This file lists tools managed by Aftman, a cross-platform toolchain manager. -# For more information, see https://github.com/LPGhatguy/aftman - -# To add a new tool, add an entry to this table. -[tools] -rojo = "rojo-rbx/rojo@7.3.0" -wally = "UpliftGames/wally@0.3.1" -wally-package-types = "JohnnyMorganz/wally-package-types@1.2.1" diff --git a/build.project.json b/build.project.json index f2adb3d..b1503ca 100644 --- a/build.project.json +++ b/build.project.json @@ -1,9 +1,6 @@ { - "name": "Packages", - "tree": { - "$path": "Packages", - "Future": { - "$path": "lib" - } - } -} \ No newline at end of file + "name": "luau-futures", + "tree": { + "$path": "dist/src" + } +} diff --git a/default.project.json b/default.project.json index 44b7790..cd066e1 100644 --- a/default.project.json +++ b/default.project.json @@ -1,6 +1,22 @@ { - "name": "Future", - "tree": { - "$path": "lib" - } -} \ No newline at end of file + "name": "luau-futures", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "DevPackages": { + "$path": "DevPackages" + }, + "Packages": { + "$className": "Folder", + "Project": { + "$path": "src" + } + } + }, + "ServerScriptService": { + "run-tests": { + "$path": "scripts/run-tests.server.luau" + } + } + } +} diff --git a/dev.project.json b/dev.project.json new file mode 100644 index 0000000..2cd638a --- /dev/null +++ b/dev.project.json @@ -0,0 +1,22 @@ +{ + "name": "luau-futures", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "DevPackages": { + "$path": "DevPackages" + }, + "Packages": { + "$className": "Folder", + "Project": { + "$path": "dist/src" + } + } + }, + "ServerScriptService": { + "run-tests": { + "$path": "dist/run-tests.server.luau" + } + } + } +} diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..c1d1024 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,40 @@ +--- +title: Installation +description: How to install Luau Futures +sidebar_position: 2 +--- + +Luau Futures has support for any Luau environment, relying only +on the coroutine library. + +We support installing the library from either wally or pesde, with wally +being used internally for development. + +## Wally + +```toml +[dependencies] +Futures = "yetanotherclown/luau-futures@^2.0.0" +``` + +:::note +Wally does not export types automatically and will display a type-error in one of the Dependencies. + +To fix this, see https://github.com/JohnnyMorganz/wally-package-types. +::: + +## Pesde + +```toml +[dependencies] +Futures = { name = "yetanotherclown/luau_futures", version = "^2.0.0", target = "luau" } +``` + +Or, you can run `pesde add yetanotherclown/luau_futures --target luau --alias Futures`. + +## GitHub Releases + +You can also find .rbxm files for use within Studio directly +within GitHub Releases. + +Get the [Latest Release](https://github.com/YetAnotherClown/luau-futures/releases/latest). \ No newline at end of file diff --git a/docs/intro.mdx b/docs/intro.mdx new file mode 100644 index 0000000..b8996b4 --- /dev/null +++ b/docs/intro.mdx @@ -0,0 +1,66 @@ +--- +title: Luau Futures +description: An introduction to Luau Futures +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +A future represents a read-only asynchronous value, one that may +not have finished computation yet. A basic future could look like: + +```lua +local Futures = require("@packages/Futures") +local Future = Futures.Future + +local myFuture = Future.new(function() + yield() + return 1, 2, 3 +end) +``` + +When you create a future, it wont begin execution until it is either +polled or awaited. + +Polling will advance the future to it's next resumption point every +time that it is called, returning a [Poll] to let you check the status +of the future. + +If the Poll is ready, you can also unwrap it to get the [Result]. +```lua +local poll = myFuture:poll() +if poll:isReady() then + local result = poll:unwrap() + -- Handle result +end +``` + +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. + +```lua +local result = myFuture:await() +-- Handle result +``` + +To read the result, you can use [Result:isOk] or [Result:isErr] to +check what type the Result is. + +You can then use [Result:unwrapOk] or [Result:unwrapErr] to get the +value of the result. + +```lua +if result:isOk() then + print(result:unwrapOk()) -- 1, 2, 3 +elseif result:isErr() then + warn(result:unwrapErr()) -- An error occurred +end +``` + +There are also several other methods for chaining, combining, and +mapping futures, as well as other utilities for working with futures. + +It is suggested to read the [API Documentation](/api/Future) for more +information about these methods. \ No newline at end of file diff --git a/docs/typechecking.md b/docs/typechecking.md new file mode 100644 index 0000000..64ac004 --- /dev/null +++ b/docs/typechecking.md @@ -0,0 +1,66 @@ +--- +title: Typechecking Guide +description: A guide on Typechecking Futures +--- + +Currently, in the Luau type solver, there are restrictions placed on recursive types. Luau Futures works around this, by allowing you to chain up +to 3 recursive functions (andThen, map, join, etc.) before the types are exhausted. + +## Future vs FutureLike Types + +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. + +Example usage, +```lua +function Class:method(future: Futures.FutureLike): Futures.Future + return future:andThen(function(...) + -- ... + end) :: any +end +``` + +:::note +To avoid recursive type restrictions, there are internally multiple types like +`FutureFirst`, `FutureNext`, `FutureLast` and `FutureExhausted`. + +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()`. + +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. +::: + +In the Future, `Futures.FutureLike` will be deprecated, but it will remain +available for backwards compatibility. + +## Join Methods + +Currently, the Luau type solver cannot properly type the join methods. +So for now, they will always return an exhausted future. + +## UnwrapOrElse + +[Future:unwrapOrElse] should return the type `Future`. However, +due to recursive type restrictions, it will return `Future`. + +You should make a mental note that using this method will never error, and +write your code accordingly. + +## Getting Around Exhaustion + +So, you have an exhausted future that you want to typecheck. +You can use type annotations and type casts to get around exhausted +futures. + +```lua +local newFuture: Future = exhaustedFuture:andThen(function(...) + -- ... + return 1, 2, 3 +end) :: any +``` \ No newline at end of file diff --git a/lib/Output.lua b/lib/Output.lua deleted file mode 100644 index b3c11f4..0000000 --- a/lib/Output.lua +++ /dev/null @@ -1,52 +0,0 @@ -local Result = require(script.Parent.Result) - -type Result = Result.Result - ---[=[ - @method output - @within Future - - When the Result of the Future is ready, calling the ``output`` method will return a [Result](https://yetanotherclown.github.io/Luau-Future/api/Result). - - @return Result -]=] - -local Output = {} -Output.__index = Output - -function Output:__call(): Result - return self.result -end - -function Output:poll(): boolean - if not self.result then - return false - end - - return true -end - -function Output.new(callback: (T) -> T | E, ...: T): Output - local self = setmetatable({}, Output) - - local success, results = xpcall( - function(...) - return { callback(...) } - end, - function(err) - return { err } - end, ... - ) - - local enumType = success and "Ok" or "Error" :: "Ok" | "Error" - - self.result = Result.new(enumType, results) - - return self -end - -export type Output = typeof(setmetatable({ - result = Result.new("Ok", {}) :: Result -}, Output)) - -return Output \ No newline at end of file diff --git a/lib/Result.lua b/lib/Result.lua deleted file mode 100644 index 717e4c6..0000000 --- a/lib/Result.lua +++ /dev/null @@ -1,94 +0,0 @@ ---[=[ - @class Result - - The result of a Future, either equal to the return values or an error message. - - ### Checking the Result - - You can retrieve either the Returned Values (as a tuple), or the Error Message (as a string) - by using the ``unwrap`` method or by calling the result as a function. - ```lua - local result = myFuture:output() - - if result:ok() then - local returnedValues = result:unwrap() - - -- Do something - elseif result:error() then - warn(result:unwrap()) - end - ``` -]=] - -local Result = {} -Result.__index = Result - -function Result:__call(): (T | E)? - return self:unwrap() :: (T | E)? -end - ---[=[ - @method ok - @within Result - - Returns whether the Result has returned the values of the Future successfully or not. - - @return boolean -]=] -function Result:ok(): boolean - if self.enumType ~= "Ok" then - return false - end - - return true -end - ---[=[ - @method error - @within Result - - If the Result is an Error, returns true. - - @return boolean -]=] -function Result:error(): boolean - if self.enumType ~= "Error" then - return false - end - - return true -end - ---[=[ - @method unwrap - @within Result - - Returns the stored values within the result as a tuple or an error (as a string). - - @return (T | E)? -]=] -function Result:unwrap(): (T | E)? - if not self.enumValue then - return nil - end - - return table.unpack(self.enumValue) -end - -function Result.new(enumType: "Ok" | "Error", results: T | E): Result - local self = setmetatable({}, Result) - - self.type = "Enum" - self.enumType = enumType :: "Ok" | "Error" - self.enumValue = results - - return self -end - -export type Result = typeof(setmetatable({ - type = "Enum", - enumType = "Ok" :: "Ok" | "Error", - enumValue = nil :: {T | E}? -}, Result)) - -return Result \ No newline at end of file diff --git a/lib/init.lua b/lib/init.lua deleted file mode 100644 index d4feec8..0000000 --- a/lib/init.lua +++ /dev/null @@ -1,129 +0,0 @@ ---!strict - ---[[ - Future 1.1.0 - A Minimal, Typed Future Implementation inspired by the concept of Futures from the Rust Ecosystem. - - https://yetanotherclown.github.io/Luau-Future/ -]] - -local Package = script.Parent - -local Output = require(script.Output) -type Output = Output.Output - -local ThreadPool = require(Package.ThreadPool) -type ThreadPool = ThreadPool.ThreadPool - -local threadPool = ThreadPool.new() - ---[=[ - @class Future - - A Minimal, Typed Future Implementation inspired by the concept of Futures from the Rust Ecosystem. - - ### Creating a Future - - To create a Future, use the ``new`` function or call the Library as a function and provide a yielding function as a parameter. - ```lua - local Future = require(path.to.module) - - local myFuture = Future.new(function(...) - -- Something that yields - end, ...) - ``` - - ### Polling a Future - - To check if a Future is ready with it's results, you poll it! You can either call the ``isReady()`` - or ``isPending`` methods to check whether or not the Future is ready. - ```lua - -- Poll the Future to see if it is ready. - if myFuture:isReady() then - -- Do something - end - - -- Poll the Future to see if it is pending. - if myFuture:isPending() then - -- Future is still pending! - end - ``` - - ### Retrieving Output - - To retrieve the Output of a Future, call the ``output()`` method on the Future when it is ready. - ```lua - if myFuture:isReady() then - local result = myFuture:output() - - if result:ok() then - local a, b, c... = result:unwrap() - - -- Do something - elseif result:error() then - warn(result:unwrap()) - end - end - ``` - - :::warning - It is important to Poll a Future first instead of just calling ``output()``! - - Your Future could no longer be Pending but not return anything, in which case - ``output()`` will return ``nil``. - ::: -]=] -local Future = {} -Future.__index = Future - -function Future:__call(callback: (T) -> T | E, ...: T): Future - return self.new(callback) :: Future -end - ---[=[ - @method isPending - @within Future - - Polls the Future and returns ``true`` if the Future is pending. - - @return boolean -]=] -function Future:isPending(): boolean - return not self:isReady() -end - ---[=[ - @method isReady - @within Future - - Polls the Future and returns ``true`` if the Future is ready. - - @return boolean -]=] -function Future:isReady(): boolean - return self.output and self.output:poll() or false -end - ---[=[ - @function new - @within Future - - Creates a new future from the provided callback. - - @return Future -]=] -function Future.new(callback: (T) -> T | E, ...: T): Future - local newFuture = setmetatable({}, Future) - - threadPool:spawn(function(...) - newFuture.output = Output.new(callback, ...) - end, ...) - - return newFuture -end - -export type Future = typeof(setmetatable({ - output = nil :: Output?, -}, Future)) - -return setmetatable({}, Future) diff --git a/moonwave.toml b/moonwave.toml index e11cd90..2316958 100644 --- a/moonwave.toml +++ b/moonwave.toml @@ -1,26 +1,16 @@ -title = "Luau-Future" -gitSourceBranch = "main" +title = "Luau Futures" [docusaurus] -url = "https://yetanotherclown.github.io/" -baseUrl = "/Luau-Future" +tagline = "Rust-like futures for Luau" -[[navbar.items]] -href = "https://discord.gg/gMWmuaZEY6" -label = "Discord" -position = "right" +[home] +enabled = true [footer] -style = "dark" -copyright = "Copyright © 2023 YetAnotherClown. Built with Moonwave and Docusaurus" - -[[footer.links]] -title = "Links" +copyright = "Copyright © 2024 YetAnotherClown. Built with Moonwave and Docusaurus" -[[footer.links.items]] -label = "Clown's Circus Discord" -href = "https://discord.gg/gMWmuaZEY6" - -[[footer.links.items]] -label = "YetAnotherClown on Github" -href = "https://github.com/YetAnotherClown" \ No newline at end of file +[[navbar.items]] +title = "Discord" +href = "https://discord.gg/nKCV5fjEvH" +position = "right" +className = "discord-logo-link" diff --git a/pages/index.js b/pages/index.js new file mode 100644 index 0000000..82a4d9e --- /dev/null +++ b/pages/index.js @@ -0,0 +1,113 @@ +import Link from "@docusaurus/Link"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Layout from "@theme/Layout"; +import clsx from "clsx"; +import React from "react"; +import styles from "./index.module.css"; + +const FEATURES = [ + { + title: "Lazy, not Eager.", + description: ( + <> +

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

+

+ 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. +

+ + ), + }, + { + title: "Rusty. Rust-like.", + description: ( + <> + Luau Futures aim to have similar API and behavior to Rust Futures where possible, drawing inspiration + otherwise. + + ), + }, + { + title: "Types? Check. Typechecked.", + description: ( + <> + Luau Futures are strictly typed, within the type solver's restrictions. + + + ), + }, +]; + +function FeatureIcon({ icon }) { + return
{icon}
; +} + +function Feature({ title, description }) { + return ( +
+
+
+

{title}

+
+
{description}
+
+
+ ); +} + +function HomepageFeatures() { + return ( +
+
+ {FEATURES.map((props, idx) => ( + + ))} +
+
+ ); +} + +function HeroBanner() { + const { siteConfig } = useDocusaurusContext(); + + return ( +
+
+

+ {siteConfig.title} +

+

+ {siteConfig.tagline} +

+ +
+
+ ); +} + +export default function Homepage() { + const { siteConfig } = useDocusaurusContext(); + return ( + +
+ + +
+
+ ); +} diff --git a/pages/index.module.css b/pages/index.module.css new file mode 100644 index 0000000..e69de29 diff --git a/pesde.lock b/pesde.lock new file mode 100644 index 0000000..560328e --- /dev/null +++ b/pesde.lock @@ -0,0 +1,3 @@ +name = "yetanotherclown/luau_futures" +version = "0.1.0" +target = "luau" diff --git a/pesde.toml b/pesde.toml new file mode 100644 index 0000000..e925cdd --- /dev/null +++ b/pesde.toml @@ -0,0 +1,21 @@ +name = "yetanotherclown/luau_futures" +version = "0.1.0" +description = "Rust-like futures for Luau" +authors = ["YetAnotherClown"] +repository = "https://github.com/YetAnotherClown/luau-futures" +license = "MIT" +includes = [ + "!src/__*__", + "!src/__*__/*", + "!src/jest.config.luau", + "src", + "pesde.lock", +] + +[target] +environment = "luau" +lib = "src/init.luau" +build_files = ["src"] + +[indices] +default = "https://github.com/daimond113/pesde-index" diff --git a/rokit.toml b/rokit.toml new file mode 100644 index 0000000..5582972 --- /dev/null +++ b/rokit.toml @@ -0,0 +1,12 @@ +# This file lists tools managed by Rokit, a toolchain manager for Roblox projects. +# For more information, see https://github.com/rojo-rbx/rokit + +# New tools can be added by running `rokit add ` in a terminal. + +[tools] +rojo = "rojo-rbx/rojo@7.4.4" +run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" +wally = "upliftGames/wally@0.3.2" +darklua = "seaofvoices/darklua@0.14.0" +wally-package-types = "JohnnyMorganz/wally-package-types@1.3.2" +stylua = "JohnnyMorganz/StyLua@2.0.1" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..f70750d --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +# If Packages aren't installed, install them. +if [ ! -d "DevPackages" ]; then + sh scripts/install-packages.sh +fi + +rojo sourcemap default.project.json -o sourcemap.json + +darklua process --config .darklua.json src/ dist/src +rojo build build.project.json -o JestLuaProject.rbxm \ No newline at end of file diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100644 index 0000000..6ec7e77 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +# If Packages aren't installed, install them. +if [ ! -d "DevPackages" ]; then + sh scripts/install-packages.sh +fi + +rojo serve dev.project.json \ + & rojo sourcemap default.project.json -o sourcemap.json --watch \ + & darklua process --config .darklua.json --watch src/ dist/src \ + & NOCOLOR=1 darklua process --config .darklua.json --watch scripts/run-tests.server.luau dist/run-tests.server.luau \ No newline at end of file diff --git a/scripts/install-packages.sh b/scripts/install-packages.sh new file mode 100644 index 0000000..c0fd665 --- /dev/null +++ b/scripts/install-packages.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +wally install \ + && wally-package-types --sourcemap sourcemap.json DevPackages/ \ No newline at end of file diff --git a/scripts/publish-pesde.sh b/scripts/publish-pesde.sh new file mode 100644 index 0000000..9937617 --- /dev/null +++ b/scripts/publish-pesde.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +rojo sourcemap default.project.json -o sourcemap.json +darklua process --config .darklua.json src/ dist/src + +cp README.md dist/README.md +cp LICENSE dist/LICENSE +cp pesde.toml dist/pesde.toml +cp pesde.lock dist/pesde.lock + +cp build.project.json dist/default.project.json +sed -i 's/dist\/src/src/' dist/default.project.json + +cd ./dist + +if [ "$1" = "--publish" ]; then + pesde publish +else + pesde publish -d +fi \ No newline at end of file diff --git a/scripts/publish-wally.sh b/scripts/publish-wally.sh new file mode 100644 index 0000000..3288fcd --- /dev/null +++ b/scripts/publish-wally.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +rojo sourcemap default.project.json -o sourcemap.json +darklua process --config .darklua.json src/ dist/src + +cp README.md dist/README.md +cp LICENSE dist/LICENSE +cp wally.toml dist/wally.toml +cp wally.lock dist/wally.lock + +cp build.project.json dist/default.project.json +sed -i 's/dist\/src/src/' dist/default.project.json + +cd ./dist + +if [ "$1" = "--publish" ]; then + wally publish +else + wally package --output release.zip +fi \ No newline at end of file diff --git a/scripts/run-tests.server.luau b/scripts/run-tests.server.luau new file mode 100644 index 0000000..3df3be8 --- /dev/null +++ b/scripts/run-tests.server.luau @@ -0,0 +1,19 @@ +_G.NOCOLOR = _G.NOCOLOR + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Packages = ReplicatedStorage.Packages + +local Jest = require("@DevPackages/Jest") + +local runCLIOptions = { + verbose = true, + ci = false, +} + +local projects = { + Packages.Project, +} + +Jest.runCLI(script, runCLIOptions, projects):await() + +return nil diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..c502086 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +OUTPUT=JestLuaProject.rbxl + +# If Packages aren't installed, install them. +if [ ! -d "DevPackages" ]; then + sh scripts/install-packages.sh +fi + +rojo sourcemap default.project.json -o sourcemap.json \ + && darklua process --config .darklua.json src/ dist/src \ + && darklua process --config .darklua.json scripts/run-tests.server.luau dist/run-tests.server.luau \ + && rojo build dev.project.json --output $OUTPUT \ + && run-in-roblox --place $OUTPUT --script dist/run-tests.server.luau \ No newline at end of file diff --git a/selene.toml b/selene.toml index b76611d..22aa064 100644 --- a/selene.toml +++ b/selene.toml @@ -1 +1,4 @@ -std = "Roblox" \ No newline at end of file +std = "selene_definitions" + +[rules] +global_usage = "allow" diff --git a/selene_definitions.yml b/selene_definitions.yml new file mode 100644 index 0000000..4bd464f --- /dev/null +++ b/selene_definitions.yml @@ -0,0 +1,7 @@ +base: roblox +name: selene_defs +globals: + # override Roblox require style with string requires + require: + args: + - type: string \ No newline at end of file diff --git a/src/Future.luau b/src/Future.luau new file mode 100644 index 0000000..4c2b691 --- /dev/null +++ b/src/Future.luau @@ -0,0 +1,375 @@ +local Poll = require("./Poll") +export type Poll = Poll.Poll + +local Result = require("./Result") +export type Result = Result.Result + +--- @class Future +--- +--- A Future represents a read-only asynchronous value, one that may not +--- have finished computation. +--- +--- Futures are lazy in their computation, meaning execution will not begin +--- until [Future:poll] or [Future:await] is used. +--- +--- ## Types +--- +--- You should refer to the [Typechecking Guide](/docs/typechecking) for more information. +--- +--- ### Future +--- +--- This type should only be used when returning a Future from a function, +--- using Futures.FutureLike will not provide intellisense. +--- +--- ### FutureLike +--- +--- This type should be used to typecheck function parameters, using +--- Futures.Future will not work as expected. +--- +local Future = {} +Future.__index = Future + +--- @method after +--- @within Future +--- @param fn (Result) -> Future +--- @return Future +--- +--- After completion, passes the Result of the current future +--- to the closure, returning a new Future. +function Future:after(fn) + -- DEVIATION: `then` is a reserved keyword in Luau, + -- this method is equivalent to FutureExt::then + return self.new(function() + local future = fn(self:await()) + local result = future:await() + + if result:isOk() then + return result:unwrapOk() + elseif result:isErr() then + error(result:unwrapErr(), 0) + end + end) +end + +--- @method andThen +--- @within Future +--- @param fn (U...) -> Future +--- @return Future +--- +--- After successfully resolving, create and execute another Future +--- created within the closure, with the Ok result passed in the +--- closure arguments, otherwise it is never executed. +function Future:andThen(fn) + return self.new(function() + local result = self:await() + + if result:isOk() then + local newFuture = fn(result:unwrapOk()) + local newResult = newFuture:await() + + if newResult:isOk() then + return newResult:unwrapOk() + elseif newResult:isErr() then + error(newResult:unwrapErr(), 0) + end + elseif result:isErr() then + error(result:unwrapErr(), 0) + end + end) +end + +--- @method await +--- @within Future +--- @yields +--- @return Result +--- +--- Yields until the Future finishes execution, then returns the result. +--- +--- :::warning +--- Because this is a yielding method, it is suggested that it is only +--- used within Futures, as opposed to the main thread. \ +--- \ +--- See [Future:poll] for the recommended way of executing Futures. +--- ::: +function Future:await() + local poll = self:poll() + + if not poll:isReady() then + table.insert(self._yieldedThreads, coroutine.running()) + coroutine.yield() + end + + poll = self:poll() + return poll:unwrap() +end + +--- @method inspectErr +--- @within Future +--- @param fn (E) -> () +--- @return Future +--- +--- Allows you to read the error value of a Future before passing it on. +function Future:inspectErr(fn) + table.insert(self._onCompletion, { + on = "Err", + type = "inspect", + fn = fn, + }) + + return self +end + +--- @method inspectOk +--- @within Future +--- @param fn (U...) -> () +--- @return Future +--- +--- Allows you to read the success value of a Future before passing it on. +function Future:inspectOk(fn) + table.insert(self._onCompletion, { + on = "Ok", + type = "inspect", + fn = fn, + }) + + return self +end + +--- @method join +--- @within Future +--- @param otherFuture Future +--- @return Future | (U..., T...) }> +--- +--- Joins the results of two futures into a table. +--- +--- Futures of different types, Err and Ok will still +--- have their results joined into a table. +--- +--- Results of type Ok will be unwrapped in the table, +--- whereas Results of type Err will not be unwrapped +--- and will be added as `Result` in the table. +function Future:join(otherFuture) + return self.new(function() + local result = self:await() + local otherResult = otherFuture:await() + + -- unwrapOk and unwrapErr are functionally similar + local results = result:isOk() and { result:unwrapOk() } or { result } + + if otherResult:isOk() then + for _, v in { otherResult:unwrapOk() } do + table.insert(results, v) + end + else + table.insert(results, otherResult) + end + + return results + end) +end + +--- @method joinAll +--- @within Future +--- @param ... Future +--- @return Future | ...any }> +--- +--- Joins the results of two or more futures into a table. +function Future:joinAll(...) + local previousFuture = self + + for i = 1, select("#", ...) do + previousFuture = self.join(previousFuture, select(i, ...)):mapOk(function(t) + return table.unpack(t) + end) + end + + return previousFuture +end + +--- @method mapErr +--- @within Future +--- @param fn (E) -> T +--- @return Future +--- +--- Maps the type of the Err result of a Future. +function Future:mapErr(fn) + table.insert(self._onCompletion, { + on = "Err", + type = "map", + fn = fn, + }) + + return self +end + +--- @method mapOk +--- @within Future +--- @param fn (U...) -> T... +--- @return Future +--- +--- Maps the type of the Ok result of a Future. +function Future:mapOk(fn) + table.insert(self._onCompletion, { + on = "Ok", + type = "map", + fn = fn, + }) + + return self +end + +--- @method orElse +--- @within Future +--- @param fn (E) -> Future +--- @return Future +--- +--- On Err, executes another Future of the same type. +function Future:orElse(fn) + return self.new(function() + local result = self:await() + + if result:isOk() then + return result:unwrapOk() + elseif result:isErr() then + local newFuture = fn(result:unwrapErr()) + local newResult = newFuture:await() + + if newResult:isOk() then + return newResult:unwrapOk() + elseif newResult:isErr() then + error(newResult:unwrapErr(), 0) + end + end + end) +end + +--- @method unwrapOrElse +--- @within Future +--- @param fn (E) -> U... +--- @return Future +--- +--- On Err, the result is passed to the closure to create a Ok result, +--- then returns a Future with that Ok result. +function Future:unwrapOrElse(fn) + return self.new(function() + local result = self:await() + + if result:isOk() then + return result:unwrapOk() + elseif result:isErr() then + return fn(result:unwrapErr()) + end + end) +end + +--- @method poll +--- @within Future +--- @return Poll +--- +--- Executes the Future on it's next resumption point, +--- returning `Result::Pending` if it is not ready yet or +--- `Result::Ready` if it is ready. +--- +--- :::danger +--- Polling a Future that is ready will return the same result. +--- This will however not be guaranteed behavior, and you should +--- avoid polling a Future that is already ready. +--- ::: +function Future:poll() + local threadExists = self.thread ~= nil + + if not threadExists then + debug.profilebegin("Create Thread") + local thread = coroutine.create(function(...) + local success, result = xpcall(function(...) + local results = { self._callback(...) } + + debug.profilebegin("Handle OK") + for _, command in self._onCompletion do + if command.on == "Err" then + continue + end + + if command.type == "map" then + results = { command.fn(table.unpack(results)) } + else + command.fn(table.unpack(results)) + end + end + debug.profileend() + + return results + end, function(err) + debug.profilebegin("Handle ERR") + for _, command in self._onCompletion do + if command.on == "Ok" then + continue + end + + if command.type == "map" then + _, err = pcall(command.fn, err) + else + command.fn(err) + end + end + debug.profileend() + + return err + end, ...) + + if not success then + result = { result } + end + + self._threadStatus = { + success = success, + result = result, + } + + for _, yieldedThread in self._yieldedThreads do + if coroutine.status(yieldedThread) ~= "suspended" then + continue + end + + coroutine.resume(yieldedThread) + end + end) + + self.thread = thread + coroutine.resume(thread, table.unpack(self._arguments)) + debug.profileend() + end + + local ready = if self._threadStatus then true else false + local success = if ready then self._threadStatus.success else false + local result = if ready then self._threadStatus.result else {} + + local resultType: ("Ok" | "Err")? = if success then "Ok" elseif ready then "Err" else nil + + if resultType == "Ok" then + return Poll.ok(table.unpack(result)) + elseif resultType == "Err" then + return Poll.err(table.unpack(result)) + else + return Poll.notReady() + end +end + +--- @method new +--- @within Future +--- @param callback (T...) -> U... +--- @param ... T... +--- @return Future +--- +--- Creates a new Future, taking an asynchronous callback and +--- parameters to pass into that callback. +function Future.new(callback, ...) + return setmetatable({ + _onCompletion = {}, + _arguments = table.pack(...), + _callback = callback, + _yieldedThreads = {}, + }, Future) +end + +return Future diff --git a/src/Poll.luau b/src/Poll.luau new file mode 100644 index 0000000..f763258 --- /dev/null +++ b/src/Poll.luau @@ -0,0 +1,143 @@ +local Result = require("./Result") +type Result = Result.Result + +type PollImpl = { + isReady: (self: Poll) -> boolean, + isPending: (self: Poll) -> boolean, + unwrap: (self: Poll) -> Result, +} + +--- @class Poll +--- +--- A Poll represents the status of a [Result], +--- whether that result is Ready or Pending. +--- +--- ```lua +--- local poll = Poll.ok(...) +--- +--- if poll:isReady() then +--- local result = poll:unwrap() +--- -- ... +--- elseif poll:isPending() then +--- -- ... +--- end +--- ``` +local Poll = {} +Poll.__index = Poll + +function Poll:__tostring() + if self:isReady() then + local result = self:unwrap() + return `Poll::Ready({result})` + elseif self:isPending() then + return "Poll::Pending" + end + + return "Poll::Unknown" +end + +export type Poll = { + _ready: boolean, + _results: { any }, + _resultType: ("Ok" | "Err")?, +} & PollImpl + +--- @within Poll +--- +--- Checks whether the Result of a Poll is ready. +--- ```lua +--- local poll = future:poll() +--- if poll:isReady() then +--- local result = poll:unwrap() +--- -- ... +--- end +--- ``` +function Poll:isReady(): boolean + return self._ready +end + +--- @within Poll +--- +--- Checks whether the Result of a Poll is pending. +--- ```lua +--- local poll = future:poll() +--- if poll:isPending() then +--- continue +--- end +--- ``` +function Poll:isPending(): boolean + return not self._ready +end + +--- @within Poll +--- @error Attempt to unwrap a pending future -- To prevent this, use Poll:isReady() or Future:await() to ensure the Result is ready. +--- +--- Checks whether the Result of a Poll is ready. +--- ```lua +--- local poll = future:poll() +--- if poll:isReady() then +--- local result = poll:unwrap() +--- -- ... +--- else +--- local result = poll:unwrap() --! Errors +--- -- Error: Attempt to unwrap a pending future +--- end +--- ``` +function Poll:unwrap(): Result + if not self._ready then + error("Attempt to unwrap a pending future, use Poll:isReady() or Future:await() to avoid this error.") + else + if self._resultType == "Ok" then + return Result.ok(table.unpack(self._results)) :: any + else + return Result.err(table.unpack(self._results)) :: any + end + end +end + +--- @within Poll +--- +--- Creates a Poll with an `Ok` result. +function Poll.ok(...: U...): Poll + local poll = setmetatable({ + _ready = true, + _resultType = "Ok", + _results = table.pack(...), + }, Poll) :: any + + return poll +end + +--- @within Poll +--- +--- Creates a Poll with an `Err` result. +function Poll.err(errValue: E): Poll + local poll = setmetatable({ + _ready = true, + _resultType = "Err", + _results = { errValue }, + }, Poll) :: any + + return poll +end + +--- @within Poll +--- +--- Creates a Poll which status is not ready. +function Poll.notReady(): Poll + local poll = setmetatable({ + _ready = false, + _resultType = nil, + _results = {}, + }, Poll) :: any + + return poll +end + +type Library = { + ok: (U...) -> Poll, + err: (errValue: E) -> Poll, + notReady: () -> Poll, +} + +return (Poll :: any) :: Library diff --git a/src/Result.luau b/src/Result.luau new file mode 100644 index 0000000..7fba4fb --- /dev/null +++ b/src/Result.luau @@ -0,0 +1,114 @@ +local utils = require("@Project/utils") +local prettyPrint = utils.prettyPrint + +type ResultType = "Ok" | "Err" + +type ResultImpl = { + isOk: (self: Result) -> boolean, + isErr: (self: Result) -> boolean, + unwrapOk: (self: Result) -> U..., + unwrapErr: (self: Result) -> E, +} + +--- @class Result +--- +--- A Result type for use with Futures, with API designed +--- to support strict typing. +local Result = {} +Result.__index = Result + +function Result:__tostring() + local results = prettyPrint(table.unpack(self._results)) + + if self:isOk() then + return `Result::Ok({results})` + elseif self:isErr() then + return `Result::Err({results})` + end + + return "Result::Unknown" +end + +export type Result = { + _type: ResultType, + _results: { any }, +} & ResultImpl + +--- @within Result +--- +--- Checks whether the Result is of the `Ok` type. +function Result:isOk(): boolean + return self._type == "Ok" +end + +--- @within Result +--- +--- Checks whether the Result is of the `Err` type. +function Result:isErr(): boolean + return self._type == "Err" +end + +--- @within Result +--- +--- Unwraps the results of an `Ok` Result. +--- +--- :::danger +--- Make sure you use [Result:isOk] before using this method. +--- +--- ```lua +--- if result:isOk() then +--- local result = result:unwrapOk() +--- end +--- ``` +--- ::: +function Result:unwrapOk(): U... + return table.unpack(self._results :: any) +end + +--- @within Result +--- +--- Unwraps the results of an `Err` Result. +--- +--- :::danger +--- Make sure you use [Result:isErr] before using this method. +--- +--- ```lua +--- if result:isErr() then +--- local err = result:unwrapErr() +--- end +--- ``` +--- ::: +function Result:unwrapErr(): E + return table.unpack(self._results) +end + +--- @within Result +--- +--- Creates a new Result of the `Ok` type. +function Result.ok(...: U...): Result + local result = setmetatable({ + _type = "Ok", + _results = table.pack(...), + }, Result) :: any + + return result +end + +--- @within Result +--- +--- Creates a new Result of the `Err` type. +function Result.err(errValue: E): Result + local result = setmetatable({ + _type = "Err", + _results = { errValue }, + }, Result) :: any + + return result +end + +type Library = { + ok: (U...) -> Result, + err: (E) -> Result, +} + +return (Result :: any) :: Library diff --git a/src/__tests__/after.test.luau b/src/__tests__/after.test.luau new file mode 100644 index 0000000..160f82c --- /dev/null +++ b/src/__tests__/after.test.luau @@ -0,0 +1,43 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local describe = JestGlobals.describe +local test = JestGlobals.test +local expect = JestGlobals.expect +local jest = JestGlobals.jest + +local Futures = require("../init") +local Future = Futures.Future + +describe("after", function() + test("success", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + local _mockSuccess, onSuccess = nil, function() + return 1, 2, 3 + end + + local mockFailure, onFailure = jest.fn(function() + error("This should never be called") + end) + + local nextFuture = myFuture:after(function(result) + if result:isOk() then + return Future.new(onSuccess) + else + return Future.new(onFailure :: any) + end + end) + + local result = nextFuture:await() + expect(result:isOk()).toBe(true) + + -- FUTURE: use jest.spyOn + -- expect(mockSuccess).toHaveBeenCalled() + expect(mockFailure).never.toHaveBeenCalled() + + local values = { result:unwrapOk() } + expect(values).toEqual({ 1, 2, 3 }) + end) +end) diff --git a/src/__tests__/andThen.test.luau b/src/__tests__/andThen.test.luau new file mode 100644 index 0000000..13026cd --- /dev/null +++ b/src/__tests__/andThen.test.luau @@ -0,0 +1,26 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("andThen", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + local nextFuture = myFuture:andThen(function() + return Future.new(function() + return 1, 2, 3 + end) + end) + + local result = nextFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ 1, 2, 3 }) +end) diff --git a/src/__tests__/await.test.luau b/src/__tests__/await.test.luau new file mode 100644 index 0000000..79b302a --- /dev/null +++ b/src/__tests__/await.test.luau @@ -0,0 +1,37 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local describe = JestGlobals.describe +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +describe("await", function() + test("immediate", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ "a", "b", "c" }) + end) + + test("yielding", function() + local myFuture = Future.new(function() + wait() + return "a", "b", "c" + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ "a", "b", "c" }) + end) +end) diff --git a/src/__tests__/chaining.test.luau b/src/__tests__/chaining.test.luau new file mode 100644 index 0000000..cbc9885 --- /dev/null +++ b/src/__tests__/chaining.test.luau @@ -0,0 +1,45 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("chaining", function() + expect.assertions(6) + + local myFuture = Future.new(function() + return 1, 2, 3 + end) + :andThen(function(a, b, c) + return Future.new(function() + expect({ a, b, c }).toEqual({ 1, 2, 3 }) + return tostring(a), tostring(b), tostring(c) + end) + end) + :inspectOk(function(a, b, c) + expect({ a, b, c }).toEqual({ "1", "2", "3" }) + end) + :mapErr(function(_err) + return "I don't expect an error here" + end) + :mapOk(function(a, b, c) + expect({ a, b, c }).toEqual({ "1", "2", "3" }) + return false, true, 22 + end) + :inspectOk(function(a, b, c) + expect({ a, b, c }).toEqual({ false, true, 22 } :: { any }) + end) + :orElse(function(_err) + return Future.new(function() + return false, true, 21 + end) :: any + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + expect(values).toEqual({ false, true, 22 } :: { any }) +end) diff --git a/src/__tests__/inspectError.test.luau b/src/__tests__/inspectError.test.luau new file mode 100644 index 0000000..6a7572c --- /dev/null +++ b/src/__tests__/inspectError.test.luau @@ -0,0 +1,23 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("inspectErr", function() + local myFuture = Future.new(function() + error("A", 0) + end) + + myFuture:inspectErr(function(...) + expect(...).toEqual("A") + end) + + local result = myFuture:await() + expect(result:isErr()).toBe(true) + + expect(result:unwrapErr()).toEqual("A") + expect.assertions(3) +end) diff --git a/src/__tests__/inspectOk.test.luau b/src/__tests__/inspectOk.test.luau new file mode 100644 index 0000000..884a304 --- /dev/null +++ b/src/__tests__/inspectOk.test.luau @@ -0,0 +1,26 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("inspectOk", function() + expect.assertions(3) + + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + myFuture:inspectOk(function(...) + expect({ ... }).toEqual({ "a", "b", "c" }) + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ "a", "b", "c" }) +end) diff --git a/src/__tests__/join.test.luau b/src/__tests__/join.test.luau new file mode 100644 index 0000000..73354b0 --- /dev/null +++ b/src/__tests__/join.test.luau @@ -0,0 +1,45 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local describe = JestGlobals.describe +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +describe("join", function() + test("ok", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + local nextFuture = myFuture:join(Future.new(function() + return "d", "e", "f" + end)) + + local result = nextFuture:await() + expect(result:isOk()).toBe(true) + + local values = result:unwrapOk() + + expect(values).toEqual({ "a", "b", "c", "d", "e", "f" }) + end) + + test("err", function() + local myFuture = Future.new(function() + error("A", 0) + end) + + local nextFuture = myFuture:join(Future.new(function() + error("B", 0) + end)) + + local result = nextFuture:await() + expect(result:isOk()).toBe(true) + + local values = result:unwrapErr() + + expect(values[1]:unwrapErr()).toEqual("A") + expect(values[2]:unwrapErr()).toEqual("B") + end) +end) diff --git a/src/__tests__/joinAll.test.luau b/src/__tests__/joinAll.test.luau new file mode 100644 index 0000000..0d3ecc1 --- /dev/null +++ b/src/__tests__/joinAll.test.luau @@ -0,0 +1,38 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("joinAll", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + local nextFuture = myFuture:joinAll( + Future.new(function() + return "d", "e", "f" + end), + Future.new(function() + return "g", "h", "i" + end), + Future.new(function() + return "j", "k", "l" + end) + ) + + local result = nextFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + -- stylua: ignore + expect(values).toEqual({ + "a", "b", "c", + "d", "e", "f", + "g", "h", "i", + "j", "k", "l", + }) +end) diff --git a/src/__tests__/mapError.test.luau b/src/__tests__/mapError.test.luau new file mode 100644 index 0000000..27eed33 --- /dev/null +++ b/src/__tests__/mapError.test.luau @@ -0,0 +1,47 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local describe = JestGlobals.describe +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +describe("mapErr", function() + test("to error", function() + local myFuture = Future.new(function() + error("A", 0) + end) + + myFuture:mapErr(function(err) + expect(err).toEqual("A") + error("B", 0) + end) + + local result = myFuture:await() + expect(result:isErr()).toBe(true) + + local err = result:unwrapErr() + expect(err).toEqual("B") + expect.assertions(3) + end) + + test("to string", function() + local myFuture = Future.new(function() + error("A", 0) + return "A" + end) + + myFuture:mapErr(function(err) + expect(err).toEqual("A") + return "B" + end) + + local result = myFuture:await() + expect(result:isErr()).toBe(true) + + local err = result:unwrapErr() + expect(err).toEqual("B") + expect.assertions(3) + end) +end) diff --git a/src/__tests__/mapOk.test.luau b/src/__tests__/mapOk.test.luau new file mode 100644 index 0000000..55c85cf --- /dev/null +++ b/src/__tests__/mapOk.test.luau @@ -0,0 +1,24 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("mapOk", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + myFuture:mapOk(function(a, b, c) + return a == "a" and 1, b == "b" and 2, c == "c" and 3 + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ 1, 2, 3 }) +end) diff --git a/src/__tests__/orElse.test.luau b/src/__tests__/orElse.test.luau new file mode 100644 index 0000000..d04128c --- /dev/null +++ b/src/__tests__/orElse.test.luau @@ -0,0 +1,28 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("orElse", function() + local myFuture = Future.new(function() + error("A", 0) + end) + + myFuture = myFuture:orElse(function(err) + expect(err).toEqual("A") + return Future.new(function() + return 1, 2, 3 + end) + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local results = { result:unwrapOk() } + expect(results).toEqual({ 1, 2, 3 }) + + expect.assertions(3) +end) diff --git a/src/__tests__/ordering.test.luau b/src/__tests__/ordering.test.luau new file mode 100644 index 0000000..730e2f9 --- /dev/null +++ b/src/__tests__/ordering.test.luau @@ -0,0 +1,59 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect +local describe = JestGlobals.describe + +local Futures = require("../init") +local Future = Futures.Future + +describe("ordering", function() + test("chaining", function() + expect.assertions(7) + + local callOrder = {} + + local myFuture = Future.new(function() + table.insert(callOrder, "A") + return 1, 2, 3 + end) + :andThen(function(a, b, c) + return Future.new(function() + table.insert(callOrder, "B") + expect({ a, b, c }).toEqual({ 1, 2, 3 }) + return tostring(a), tostring(b), tostring(c) + end) + end) + :inspectOk(function(a, b, c) + table.insert(callOrder, "C") + expect({ a, b, c }).toEqual({ "1", "2", "3" }) + end) + :mapErr(function(_err) + table.insert(callOrder, "Should not be called") + return "I don't expect an error here" + end) + :mapOk(function(a, b, c) + table.insert(callOrder, "D") + expect({ a, b, c }).toEqual({ "1", "2", "3" }) + return false, true, 22 + end) + :inspectOk(function(a, b, c) + table.insert(callOrder, "E") + expect({ a, b, c }).toEqual({ false, true, 22 } :: { any }) + end) + :orElse(function(_err) + table.insert(callOrder, "Should not be called") + return Future.new(function() + return false, true, 21 + end) :: any + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + expect(values).toEqual({ false, true, 22 } :: { any }) + + expect(callOrder).toEqual({ "A", "B", "C", "D", "E" }) + end) +end) diff --git a/src/__tests__/poll.test.luau b/src/__tests__/poll.test.luau new file mode 100644 index 0000000..9291331 --- /dev/null +++ b/src/__tests__/poll.test.luau @@ -0,0 +1,49 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local describe = JestGlobals.describe +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +describe("poll", function() + test("immediate", function() + local myFuture = Future.new(function() + return "a", "b", "c" + end) + + local poll = myFuture:poll() + expect(poll:isReady()).toBe(true) + + local result = poll:unwrap() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ "a", "b", "c" }) + end) + + test("yielding", function() + local wait = wait + local myFuture = Future.new(function() + wait(0.1) + return "a", "b", "c" + end) + + local poll = myFuture:poll() + expect(poll:isPending()).toBe(true) + + wait(0.1) + + poll = myFuture:poll() + expect(poll:isReady()).toBe(true) + + local result = poll:unwrap() + expect(result:isOk()).toBe(true) + + local values = { result:unwrapOk() } + + expect(values).toEqual({ "a", "b", "c" }) + end) +end) diff --git a/src/__tests__/unwrapOrElse.test.luau b/src/__tests__/unwrapOrElse.test.luau new file mode 100644 index 0000000..9b00e09 --- /dev/null +++ b/src/__tests__/unwrapOrElse.test.luau @@ -0,0 +1,26 @@ +local JestGlobals = require("@DevPackages/JestGlobals") + +local test = JestGlobals.test +local expect = JestGlobals.expect + +local Futures = require("../init") +local Future = Futures.Future + +test("unwrapOrElse", function() + local myFuture = Future.new(function() + error("A", 0) + end) + + myFuture = myFuture:unwrapOrElse(function(err) + expect(err).toEqual("A") + return 1, 2, 3 + end) + + local result = myFuture:await() + expect(result:isOk()).toBe(true) + + local results = { result:unwrapOk() } + expect(results).toEqual({ 1, 2, 3 }) + + expect.assertions(3) +end) diff --git a/src/init.luau b/src/init.luau new file mode 100644 index 0000000..44266de --- /dev/null +++ b/src/init.luau @@ -0,0 +1,146 @@ +local Poll = require("./Poll") +export type Poll = Poll.Poll + +local Result = require("./Result") +export type Result = Result.Result + +local Future = require("./Future") :: any + +-- FUTURE: FutureLike = Future for backwards compatibility +-- This type should be used to typecheck function parameters,\ +-- using Futures.Future will not work as expected. +export type FutureLike = { + await: (self: FutureLike) -> Result, + poll: (self: FutureLike) -> Poll, + [any]: any, +} + +-- FUTURE: Replace when recursive type restrictions are lifted +-- This type should only be used when returning a Future from a function,\ +-- Using Futures.FutureLike will not provide intellisense. +export type Future = FutureFirst + +type FutureExhausted = { + _onResolve: { (...any) -> () }, + _onErr: { (any) -> () }, + _mapOn: { + ok: { (...any) -> T... }, + err: { (any) -> T }, + }, + _arguments: { any }, + _callback: (...any) -> ...any, + _threadStatus: { success: boolean, results: { any } }, + _thread: thread?, + + after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureExhausted, + andThen: (self: FutureLike, fn: (...any) -> FutureLike) -> FutureExhausted, + await: (self: FutureLike) -> Result, + inspectErr: (self: FutureLike, fn: (any) -> ()) -> FutureExhausted, + inspectOk: (self: FutureLike, fn: (...any) -> ()) -> FutureExhausted, + join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, + joinAll: (...FutureLike) -> FutureExhausted, + mapErr: (self: FutureLike, fn: (any) -> T) -> FutureExhausted, + mapOk: (self: FutureLike, fn: (...any) -> T...) -> FutureExhausted, + orElse: (self: FutureLike, fn: (any) -> FutureExhausted) -> FutureExhausted, + unwrapOrElse: (self: FutureLike, fn: (err: any) -> ...any) -> FutureExhausted, + poll: (self: FutureLike) -> Poll, +} + +type FutureLast = { + _onResolve: { (U...) -> () }, + _onErr: { (E) -> () }, + _mapOn: { + ok: { (U...) -> T... }, + err: { (E) -> T }, + }, + _arguments: { any }, + _callback: (...any) -> U..., + _threadStatus: { success: boolean, results: { any } }, + _thread: thread?, + + after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureExhausted, + andThen: (self: FutureLike, fn: (U...) -> FutureLike) -> FutureExhausted, + await: (self: FutureLike) -> Result, + inspectErr: (self: FutureLike, fn: (E) -> ()) -> FutureLast, + inspectOk: (self: FutureLike, fn: (U...) -> ()) -> FutureLast, + join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, + joinAll: (...FutureLike) -> FutureExhausted, + mapErr: (self: FutureLike, fn: (E) -> T) -> FutureExhausted, + mapOk: (self: FutureLike, fn: (U...) -> T...) -> FutureExhausted, + orElse: (self: FutureLike, fn: (E) -> FutureLast) -> FutureLast, + unwrapOrElse: (self: FutureLike, fn: (err: E) -> U...) -> FutureLast, + poll: (self: FutureLike) -> Poll, +} + +type FutureNext = { + _onResolve: { (U...) -> () }, + _onErr: { (E) -> () }, + _mapOn: { + ok: { (U...) -> T... }, + err: { (E) -> T }, + }, + _arguments: { any }, + _callback: (...any) -> U..., + _threadStatus: { success: boolean, results: { any } }, + _thread: thread?, + + after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureLast, + andThen: (self: FutureLike, fn: (U...) -> FutureLike) -> FutureLast, + await: (self: FutureLike) -> Result, + inspectErr: (self: FutureLike, fn: (E) -> ()) -> FutureNext, + inspectOk: (self: FutureLike, fn: (U...) -> ()) -> FutureNext, + join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, + joinAll: (...FutureLike) -> FutureExhausted, + mapErr: (self: FutureLike, fn: (E) -> T) -> FutureLast, + mapOk: (self: FutureLike, fn: (U...) -> T...) -> FutureLast, + orElse: (self: FutureLike, fn: (E) -> FutureNext) -> FutureNext, + unwrapOrElse: (self: FutureLike, fn: (err: E) -> U...) -> FutureNext, + poll: (self: FutureLike) -> Poll, +} + +type FutureFirst = { + _onResolve: { (U...) -> () }, + _onErr: { (E) -> () }, + _mapOn: { + ok: { (U...) -> T... }, + err: { (E) -> T }, + }, + _arguments: { any }, + _callback: (...any) -> U..., + _threadStatus: { success: boolean, results: { any } }, + _thread: thread?, + + after: (self: FutureLike, fn: (Result) -> FutureLike) -> FutureNext, + andThen: (self: FutureLike, fn: (U...) -> FutureLike) -> FutureNext, + await: (self: FutureLike) -> Result, + inspectErr: (self: FutureLike, fn: (E) -> ()) -> FutureFirst, + inspectOk: (self: FutureLike, fn: (U...) -> ()) -> FutureFirst, + -- FUTURE: Return Future + join: (self: FutureLike, otherFuture: FutureLike) -> FutureExhausted, + -- NOTE: This might be a little tricky to type, we can supply join3, join4, join5... methods + joinAll: (...FutureLike) -> FutureExhausted, + mapErr: (self: FutureLike, fn: (E) -> T) -> FutureNext, + mapOk: (self: FutureLike, fn: (U...) -> T...) -> FutureNext, + orElse: (self: FutureLike, fn: (E) -> FutureFirst) -> FutureFirst, + -- FUTURE: Return Future + unwrapOrElse: (self: FutureLike, fn: (err: E) -> U...) -> FutureFirst, + poll: (self: FutureLike) -> Poll, +} + +type Library = { + Poll: typeof(Poll), + Result: typeof(Result), + + -- FUTURE: Replace when recursive type restrictions are lifted + Future: { + new: (callback: (T...) -> U..., T...) -> FutureFirst, + [any]: any, + }, +} + +return { + Poll = Poll, + Result = Result, + + Future = Future, +} :: Library diff --git a/src/jest.config.luau b/src/jest.config.luau new file mode 100644 index 0000000..2350a9a --- /dev/null +++ b/src/jest.config.luau @@ -0,0 +1,9 @@ +return { + testMatch = { + "**/__tests__/*.(spec|test)", + }, + testPathIgnorePatterns = { + "Packages", + "DevPackages", + }, +} diff --git a/src/utils.luau b/src/utils.luau new file mode 100644 index 0000000..a88478a --- /dev/null +++ b/src/utils.luau @@ -0,0 +1,112 @@ +-- https://stackoverflow.com/questions/7526223/how-do-i-know-if-a-table-is-an-array/52697380#52697380 +local function isArray(t) + if type(t) ~= "table" then + return false + end + + if #t > 0 then + return true + end + + for _, _ in pairs(t) do + return false + end + + return true +end + +local function printArray(arr, indentation: number): string + local output = "" + + local indent = "" + for _ = 1, indentation do + indent ..= "\t" + end + + local outerIndent = " " + if #arr ~= 0 and string.len(indent) >= 1 then + outerIndent = string.sub(indent, 1, string.len(indent) - 1) + end + + for i, v in arr do + local vOutput = if type(v) == "table" then prettyPrintTable(v, indentation + 1) else prettyPrint(v) + + output ..= `\n{indent}{vOutput},` + + if i == #arr then + output ..= `\n` + end + end + + return `\{ {output}{outerIndent}\}` +end + +local function printDictionary(dict, indentation: number): string + local output = "" + + local indent = "" + for _ = 1, indentation do + indent ..= "\t" + end + + local outerIndent = "" + if string.len(indent) >= 1 then + outerIndent = string.sub(indent, 1, string.len(indent) - 1) + end + + for k, v in dict do + local kOutput = if type(k) == "table" then prettyPrintTable(k, indentation + 1) else prettyPrint(k) + local vOutput = if type(v) == "table" then prettyPrintTable(v, indentation + 1) else prettyPrint(v) + + output ..= `\n{indent}[{kOutput}] = {vOutput},` + end + + return `\{ {output} \n{outerIndent}\}` +end + +function prettyPrintTable(t: any, indentation: number): string + if isArray(t) then + return printArray(t, indentation) + else + return printDictionary(t, indentation) + end +end + +local function prettyPrintString(str) + local singleFound = string.find(str, "'") + local doubleFound = string.find(str, '"') + + if not doubleFound then + return `"{str}"` + elseif not singleFound then + return `'{str}'` + else + str = string.gsub(str, '"', '\\"') + str = string.gsub(str, "'", "\\'") + return `"{str}"` + end +end + +function prettyPrint(...): string + local output = {} + + for i = 1, select("#", ...) do + local v = select(i, ...) + + if type(v) == "table" then + output[i] = prettyPrintTable(v, 1) + continue + elseif type(v) == "string" then + output[i] = prettyPrintString(v) + continue + end + + output[i] = tostring(v) + end + + return table.concat(output, ", ") +end + +return { + prettyPrint = prettyPrint, +} diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..caaa48d --- /dev/null +++ b/stylua.toml @@ -0,0 +1 @@ +syntax = "Luau" diff --git a/wally.lock b/wally.lock new file mode 100644 index 0000000..1d9d13a --- /dev/null +++ b/wally.lock @@ -0,0 +1,243 @@ +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "jsdotlua/boolean" +version = "1.2.7" +dependencies = [["number", "jsdotlua/number@1.2.7"]] + +[[package]] +name = "jsdotlua/chalk" +version = "0.2.1" +dependencies = [] + +[[package]] +name = "jsdotlua/collections" +version = "1.2.7" +dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"]] + +[[package]] +name = "jsdotlua/console" +version = "1.2.7" +dependencies = [["collections", "jsdotlua/collections@1.2.7"]] + +[[package]] +name = "jsdotlua/diff-sequences" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/emittery" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"]] + +[[package]] +name = "jsdotlua/es7-types" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/expect" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/instance-of" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/jest" +version = "3.6.1-rc.2" +dependencies = [["jest-core", "jsdotlua/jest-core@3.6.1-rc.2"]] + +[[package]] +name = "jsdotlua/jest-circus" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-config" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-console" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-core" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-diff" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-each" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-environment" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-environment-roblox" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-fake-timers" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-get-type" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"]] + +[[package]] +name = "jsdotlua/jest-globals" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-matcher-utils" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-message-util" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-mock" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/jest-reporters" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-roblox-shared" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-runner" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-runtime" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-snapshot" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-test-result" +version = "3.6.1-rc.2" +dependencies = [["jest-types", "jsdotlua/jest-types@3.6.1-rc.2"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/jest-types" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"]] + +[[package]] +name = "jsdotlua/jest-util" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/jest-validate" +version = "3.6.1-rc.2" +dependencies = [["chalk", "jsdotlua/chalk@0.2.1"], ["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/luau-polyfill" +version = "1.2.7" +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"]] + +[[package]] +name = "jsdotlua/luau-regexp" +version = "0.2.1" +dependencies = [] + +[[package]] +name = "jsdotlua/math" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/number" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/path" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/picomatch" +version = "0.4.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["luau-regexp", "jsdotlua/luau-regexp@0.2.1"], ["promise", "jsdotlua/promise@3.5.2"]] + +[[package]] +name = "jsdotlua/pretty-format" +version = "3.6.1-rc.2" +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"]] + +[[package]] +name = "jsdotlua/promise" +version = "3.5.2" +dependencies = [] + +[[package]] +name = "jsdotlua/react-is" +version = "17.2.0" +dependencies = [["shared", "jsdotlua/shared@17.2.0"]] + +[[package]] +name = "jsdotlua/shared" +version = "17.2.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/string" +version = "1.2.7" +dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["number", "jsdotlua/number@1.2.7"]] + +[[package]] +name = "jsdotlua/symbol-luau" +version = "1.0.1" +dependencies = [] + +[[package]] +name = "jsdotlua/throat" +version = "3.6.1-rc.2" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"]] + +[[package]] +name = "jsdotlua/timers" +version = "1.2.7" +dependencies = [["collections", "jsdotlua/collections@1.2.7"]] + +[[package]] +name = "yetanotherclown/luau-futures" +version = "2.0.0" +dependencies = [["Jest", "jsdotlua/jest@3.6.1-rc.2"], ["JestGlobals", "jsdotlua/jest-globals@3.6.1-rc.2"]] diff --git a/wally.toml b/wally.toml index bef57e5..8bd209d 100644 --- a/wally.toml +++ b/wally.toml @@ -1,13 +1,24 @@ [package] -name = "yetanotherclown/future" -description = "A Minimal, Typed Future Implementation inspired by the concept of Futures from the Rust Ecosystem." -version = "1.1.0" +name = "yetanotherclown/luau-futures" +description = "Rust-like futures for Luau" +version = "2.0.0" license = "MIT" authors = ["YetAnotherClown"] -realm = "shared" registry = "https://github.com/UpliftGames/wally-index" -exclude = ["**"] -include = ["lib", "lib/*", "default.project.json", "wally.toml", "wally.lock"] +realm = "shared" + +# publish-wally.sh will clone to /dist and run wally publish in that directory +exclude = [ + "tests", + "tests/*", + "src/__*__", + "src/__*__/*", + "package.json", + "*.tgz", + "*.zip", +] +include = ["wally.lock"] -[dependencies] -ThreadPool = "yetanotherclown/threadpool@1.0.0" +[dev-dependencies] +Jest = "jsdotlua/jest@3.6.1-rc.2" +JestGlobals = "jsdotlua/jest-globals@3.6.1-rc.2"