commit d9bd6bbd3c0347be68ddadecb3d878552290fe66 Author: Lucien Greathouse Date: Thu Jan 25 17:21:58 2018 -0800 Initial dump diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bd538e0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.lua] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false \ No newline at end of file diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..582953d --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,41 @@ +stds.roblox = { + globals = { + "game" + }, + read_globals = { + -- Roblox globals + "script", + + -- Extra functions + "tick", "warn", "spawn", + "wait", "settings", "typeof", + + -- Types + "Vector2", "Vector3", + "Color3", + "UDim", "UDim2", + "Rect", + "CFrame", + "Enum", + "Instance", + } +} + +stds.testez = { + read_globals = { + "describe", + "it", "itFOCUS", "itSKIP", + "FOCUS", "SKIP", "HACK_NO_XPCALL", + "expect", + } +} + +ignore = { + "212", -- unused arguments +} + +std = "lua51+roblox" + +files["**/*.spec.lua"] = { + std = "+testez", +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7687a11 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Lua Promise + +## License +Lua Promise is available under the terms of the Unlicense. See [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/lib/init.lua b/lib/init.lua new file mode 100644 index 0000000..83ec441 --- /dev/null +++ b/lib/init.lua @@ -0,0 +1,311 @@ +--[[ + An implementation of Promises similar to Promise/A+. +]] + +local PROMISE_DEBUG = false + +-- If promise debugging is on, use a version of pcall that warns on failure. +-- This is useful for finding errors that happen within Promise itself. +local wpcall +if PROMISE_DEBUG then + wpcall = function(f, ...) + local result = { pcall(f, ...) } + + if not result[1] then + warn(result[2]) + end + + return unpack(result) + end +else + wpcall = pcall +end + +--[[ + Creates a function that invokes a callback with correct error handling and + resolution mechanisms. +]] +local function createAdvancer(callback, resolve, reject) + return function(...) + local result = { wpcall(callback, ...) } + local ok = table.remove(result, 1) + + if ok then + resolve(unpack(result)) + else + reject(unpack(result)) + end + end +end + +local function isEmpty(t) + return next(t) == nil +end + +local Promise = {} +Promise.__index = Promise + +Promise.Status = { + Started = "Started", + Resolved = "Resolved", + Rejected = "Rejected", +} + +--[[ + Constructs a new Promise with the given initializing callback. + + This is generally only called when directly wrapping a non-promise API into + a promise-based version. + + The callback will receive 'resolve' and 'reject' methods, used to start + invoking the promise chain. + + For example: + + local function get(url) + return Promise.new(function(resolve, reject) + spawn(function() + resolve(HttpService:GetAsync(url)) + end) + end) + end + + get("https://google.com") + :andThen(function(stuff) + print("Got some stuff!", stuff) + end) +]] +function Promise.new(callback) + local promise = { + -- Used to locate where a promise was created + _source = debug.traceback(), + + -- A tag to identify us as a promise + _type = "Promise", + + _status = Promise.Status.Started, + + -- A table containing a list of all results, whether success or failure. + -- Only valid if _status is set to something besides Started + _value = nil, + + -- If an error occurs with no observers, this will be set. + _unhandledRejection = false, + + -- Queues representing functions we should invoke when we update! + _queuedResolve = {}, + _queuedReject = {}, + } + + setmetatable(promise, Promise) + + local function resolve(...) + promise:_resolve(...) + end + + local function reject(...) + promise:_reject(...) + end + + local ok, err = wpcall(callback, resolve, reject) + + if not ok and promise._status == Promise.Status.Started then + reject(err) + end + + return promise +end + +--[[ + Create a promise that represents the immediately resolved value. +]] +function Promise.resolve(value) + return Promise.new(function(resolve) + resolve(value) + end) +end + +--[[ + Create a promise that represents the immediately rejected value. +]] +function Promise.reject(value) + return Promise.new(function(_, reject) + reject(value) + end) +end + +--[[ + Returns a new promise that: + * is resolved when all input promises resolve + * is rejected if ANY input promises reject +]] +function Promise.all(...) + error("unimplemented", 2) +end + +--[[ + Is the given object a Promise instance? +]] +function Promise.is(object) + if type(object) ~= "table" then + return false + end + + return object._type == "Promise" +end + +--[[ + Creates a new promise that receives the result of this promise. + + The given callbacks are invoked depending on that result. +]] +function Promise:andThen(successHandler, failureHandler) + self._unhandledRejection = false + + -- Create a new promise to follow this part of the chain + return Promise.new(function(resolve, reject) + -- Our default callbacks just pass values onto the next promise. + -- This lets success and failure cascade correctly! + + local successCallback = resolve + if successHandler then + successCallback = createAdvancer(successHandler, resolve, reject) + end + + local failureCallback = reject + if failureHandler then + failureCallback = createAdvancer(failureHandler, resolve, reject) + end + + if self._status == Promise.Status.Started then + -- If we haven't resolved yet, put ourselves into the queue + table.insert(self._queuedResolve, successCallback) + table.insert(self._queuedReject, failureCallback) + elseif self._status == Promise.Status.Resolved then + -- This promise has already resolved! Trigger success immediately. + successCallback(unpack(self._value)) + elseif self._status == Promise.Status.Rejected then + -- This promise died a terrible death! Trigger failure immediately. + failureCallback(unpack(self._value)) + end + end) +end + +--[[ + Used to catch any errors that may have occurred in the promise. +]] +function Promise:catch(failureCallback) + return self:andThen(nil, failureCallback) +end + +--[[ + Yield until the promise is completed. + + This matches the execution model of normal Roblox functions. +]] +function Promise:await() + self._unhandledRejection = false + + if self._status == Promise.Status.Started then + local result + local bindable = Instance.new("BindableEvent") + + self:andThen(function(...) + result = {...} + bindable:Fire(true) + end, function(...) + result = {...} + bindable:Fire(false) + end) + + local ok = bindable.Event:Wait() + bindable:Destroy() + + if not ok then + error(tostring(result[1]), 2) + end + + return unpack(result) + elseif self._status == Promise.Status.Resolved then + return unpack(self._value) + elseif self._status == Promise.Status.Rejected then + error(tostring(self._value[1]), 2) + end +end + +function Promise:_resolve(...) + if self._status ~= Promise.Status.Started then + return + end + + -- If the resolved value was a Promise, we chain onto it! + if Promise.is((...)) then + -- Without this warning, arguments sometimes mysteriously disappear + if select("#", ...) > 1 then + local message = ( + "When returning a Promise from andThen, extra arguments are " .. + "discarded! See:\n\n%s" + ):format( + self._source + ) + warn(message) + end + + (...):andThen(function(...) + self:_resolve(...) + end, function(...) + self:_reject(...) + end) + + return + end + + self._status = Promise.Status.Resolved + self._value = {...} + + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedResolve) do + callback(...) + end +end + +function Promise:_reject(...) + if self._status ~= Promise.Status.Started then + return + end + + self._status = Promise.Status.Rejected + self._value = {...} + + -- If there are any rejection handlers, call those! + if not isEmpty(self._queuedReject) then + -- We assume that these callbacks will not throw errors. + for _, callback in ipairs(self._queuedReject) do + callback(...) + end + else + -- At this point, no one was able to observe the error. + -- An error handler might still be attached if the error occurred + -- synchronously. We'll wait one tick, and if there are still no + -- observers, then we should put a message in the console. + + self._unhandledRejection = true + local err = tostring((...)) + + spawn(function() + -- Someone observed the error, hooray! + if not self._unhandledRejection then + return + end + + -- Build a reasonable message + local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( + err, + self._source + ) + warn(message) + end) + end +end + +return Promise \ No newline at end of file diff --git a/rojo.json b/rojo.json new file mode 100644 index 0000000..1a78596 --- /dev/null +++ b/rojo.json @@ -0,0 +1,10 @@ +{ + "name": "lua-promise", + "servePort": 8000, + "partitions": { + "lib": { + "path": "src", + "target": "ServerScriptService.roblox-request" + } + } +} \ No newline at end of file