mirror of
				https://github.com/AmberGraceRblx/luau-promise.git
				synced 2025-10-31 02:19:17 +00:00 
			
		
		
		
	Initial dump
This commit is contained in:
		
						commit
						d9bd6bbd3c
					
				
					 6 changed files with 398 additions and 0 deletions
				
			
		
							
								
								
									
										8
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										41
									
								
								.luacheckrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								.luacheckrc
									
									
									
									
									
										Normal file
									
								
							|  | @ -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", | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 <http://unlicense.org/> | ||||||
							
								
								
									
										4
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | # Lua Promise | ||||||
|  | 
 | ||||||
|  | ## License | ||||||
|  | Lua Promise is available under the terms of the Unlicense. See [LICENSE](LICENSE) for details. | ||||||
							
								
								
									
										311
									
								
								lib/init.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								lib/init.lua
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										10
									
								
								rojo.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								rojo.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | { | ||||||
|  |   "name": "lua-promise", | ||||||
|  |   "servePort": 8000, | ||||||
|  |   "partitions": { | ||||||
|  |     "lib": { | ||||||
|  |       "path": "src", | ||||||
|  |       "target": "ServerScriptService.roblox-request" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in a new issue