├── .gitignore ├── LICENSE ├── README.md ├── deferred.lua └── deferred_test.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brian Hang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deferred-lua 2 | Lua implementation of the [Promises/A+](https://promisesaplus.com/) standard (not completely compliant due to some differences between JS and Lua) for Garry's Mod. One big difference is the `next` is used instead of `then` since `then` is a keyword in Lua. If you have never used promises before, you should check out [this explanation](https://codeburst.io/javascript-promises-explained-with-simple-real-life-analogies-dd6908092138). While this isn't JavaScript, the concepts are the same. 3 | 4 | ## Installation 5 | Place the `deferred.lua` file somewhere and include it with 6 | ```lua 7 | include("path/to/deferred.lua") 8 | ``` 9 | 10 | ## Examples 11 | Basic usage: 12 | ```lua 13 | local function fetch(url) 14 | local d = deferred.new() 15 | http.Fetch(url, function(body, size, headers, code) 16 | d:resolve(body) 17 | end, function(err) 18 | d:reject(err) 19 | end) 20 | return d 21 | end 22 | 23 | fetch("https://google.com") 24 | :next(function(body) 25 | print("Body is: ", body) 26 | end) 27 | ``` 28 | 29 | Chaining promises 30 | ```lua 31 | fetch("https://google.com") 32 | :next(function(body) 33 | print("Body is: ", body) 34 | return #body 35 | end) 36 | :next(function(length) 37 | print("And the length is: ", length) 38 | end) 39 | ``` 40 | 41 | Handling rejection 42 | ```lua 43 | fetch("https://google.com") 44 | :next(function(body) 45 | print("Body is: ", body) 46 | return #body 47 | end) 48 | :next(function(length) 49 | print("And the length is: ", length) 50 | end) 51 | :catch(function(err) 52 | print("Oops!", err) 53 | end) 54 | ``` 55 | or 56 | ```lua 57 | fetch("https://google.com") 58 | :next(function(body) 59 | print("Body is: ", body) 60 | return #body 61 | end) 62 | :next(function(length) 63 | print("And the length is: ", length) 64 | end) 65 | :next(nil, function(err) 66 | print("Oops!", err) 67 | end) 68 | ``` 69 | 70 | ## Usage 71 | ### Promise 72 | From MDN: 73 | > The `Promise` object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value. 74 | 75 | #### `Promise:catch(onRejected)` 76 | This method adds a rejection handler to the promise chain. It is equivalent to doing `Promise:next(nil, onRejected)`. 77 | 78 | #### `Promise:next(onResolved, onRejected)` 79 | See the [Promise/A+ `then` method](https://promisesaplus.com/#the-then-method). 80 | 81 | #### `Promise:reject(reason)` 82 | Rejects the promise with the given reason. 83 | 84 | #### `Promise:resolve(value)` 85 | Resolves the promise to the given value. 86 | 87 | ### deferred 88 | This is the actual library for creating promises. 89 | 90 | #### `deferred.all(promises)` 91 | Returns a `Promise` that resolves to a table where the *i*th element is what the *i*th promise in `promises` resolves to after all the promises have been resolved. The promise will reject with the reason of the first promise within `promises` to reject. If `promises` is empty, then the returned promise resolves to an empty table `{}`. 92 | 93 | Example: 94 | ```lua 95 | local function fetch(url) 96 | local d = deferred.new() 97 | http.Fetch(url, function(body) 98 | d:resolve(body) 99 | end, function(err) 100 | d:reject(err) 101 | end) 102 | return d 103 | end 104 | 105 | deferred.all({ fetch("https://google.com"), fetch("https://github.com") }) 106 | :next(PrintTable) 107 | -- HTML is printed in console... 108 | ``` 109 | 110 | #### `deferred.any(promises)` 111 | Returns a `Promise` that resolves to the value of the first resolved promise in `promises`. 112 | 113 | Example: 114 | ```lua 115 | local snails = {} 116 | for i = 1, 10 do 117 | local d = deferred.new() 118 | timer.Simple(math.random(1, 5), function() 119 | d:resolve(i) 120 | end) 121 | snails[#snails + 1] = d 122 | end 123 | 124 | deferred.any(snails):next(function(winner) 125 | print("Winner is snail #"..winner) 126 | end) 127 | -- Winner is snail #5 128 | ``` 129 | 130 | #### `deferred.each(promises, fn)` 131 | For each promise `p` in the `promises` table, as soon as `p` resolves to a value `x`, `fn` is called with `x` as the first argument, the index corresponding to `p` in `promises` as the second argument, and the length of `promises` as the third argument. This method returns a `Promise` that is resolved to `nil` after all the promises have finished. Note that this happens **sequentially**. If a promise in `promises` is the first to be rejected, then the returned promise will reject with the same reason. 132 | 133 | #### `deferred.filter(promises, filter)` 134 | This function takes a table of promises and a function `filter` and returns a promise that resolves to a table of values that satisfy `filter` in order of the given `promises`. As the *i*th promise in `promises` resolves to a value `x`, `filter` is called with `x` as the first argument, *i* as the second argument (the index of the promise), and the length of `promises` as the third argument. If `filter` returns a truthy value, then `x` as added to the table that the returned promise resolves to. If a promise in `promises` is the first to be rejected, then the returned promise will reject with the same reason. 135 | 136 | Example: 137 | ```lua 138 | local function fetch(url) 139 | local d = deferred.new() 140 | http.Fetch(url, function(body, size, headers, code) 141 | d:resolve({ url = url, code = code }) 142 | end, function(err) 143 | d:reject(err) 144 | end) 145 | return d 146 | end 147 | 148 | deferred.filter( 149 | { fetch("https://google.com"), fetch("https://httpstat.us/404") }, 150 | function(res) return res.code ~= 404 end 151 | ):next(PrintTable) 152 | -- 1: 153 | -- code = 200 154 | -- url = https://google.com 155 | ``` 156 | 157 | #### `deferred.fold(promises, fn, initial)` 158 | The promises in `promises` are evaluated **sequentially** and the left-folds the resolved value into a single value using the `fn` accumulator function. This method returns a `Promise` that resolves to the accumulated value. If a promise in `promises` is the first to be rejected, then the returned promise will reject with the same reason. See https://en.wikipedia.org/wiki/Fold_(higher-order_function) 159 | 160 | Example: 161 | ```lua 162 | local snails = {} 163 | for i = 1, 10 do 164 | local d = deferred.new() 165 | local time = math.random(1, 5) 166 | timer.Simple(time, function() 167 | d:resolve(time) 168 | end) 169 | snails[#snails + 1] = d 170 | end 171 | 172 | deferred.fold(snails, function(acc, time) return math.max(acc, time) end, 0) 173 | :next(function(longestTime) 174 | print("The slowest snail took "..longestTime.." seconds to finish.") 175 | end) 176 | -- The slowest snail took 5 seconds to finish. 177 | ``` 178 | 179 | #### `deferred.isPromise(value)` 180 | Returns `true` if `value` is a promise, `false` otherwise. 181 | 182 | #### `deferred.map(values, fn)` 183 | Given a table of values and a function `fn` that takes in one of the values as input and returns a promise, this method returns a new promise that resolves to a table where the *i*th value is the the resolved value of `fn(values[i])`. If a promise returned by `fn` is the first to be rejected, then the returned promise will reject with the same reason. See https://en.wikipedia.org/wiki/Map_(higher-order_function) 184 | 185 | Example: 186 | ```lua 187 | local urls = {"https://google.com", "https://garrysmod.com"} 188 | 189 | local function fetch(url) 190 | local d = deferred.new() 191 | http.Fetch(url, function(body, size, headers, code) 192 | d:resolve(size) 193 | end, function(err) 194 | d:reject(err) 195 | end) 196 | return d 197 | end 198 | 199 | deferred.map(urls, fetch):next(PrintTable) 200 | -- 1 = 11384 201 | -- 2 = 23297 202 | ``` 203 | 204 | #### `deferred.new()` 205 | Returns a new `Promise` object. 206 | 207 | #### `deferred.reject(reason)` 208 | Returns a new `Promise` object that immediately rejects with `reason` as the reason. 209 | 210 | #### `deferred.resolve(value)` 211 | Returns a new `Promise` object that immediately resolves to `value`. 212 | 213 | #### `deferred.some(promises, count)` 214 | Give a table of promises and a non-negative integer `count`, this method returns a promise that resolves to a table of the first `count` resolved values in the order that the promises are resolved. If a promise in `promises` is the first to be rejected, then the returned promise will reject with the same reason. 215 | 216 | Example: 217 | ```lua 218 | local snails = {} 219 | for i = 1, 10 do 220 | local d = deferred.new() 221 | timer.Simple(math.random(1, 10), function() 222 | d:resolve(i) 223 | end) 224 | snails[#snails + 1] = d 225 | end 226 | 227 | deferred.some(snails, 3):next(function(results) 228 | print("First place is snail #"..results[1]) 229 | print("Second place is snail #"..results[2]) 230 | print("Third place is snail #"..results[3]) 231 | end) 232 | 233 | -- First place is snail #6 234 | -- Second place is snail #9 235 | -- Third place is snail #5 236 | ``` 237 | 238 | ## Unhandled Rejections 239 | This is an implementation specific feature where promises that do not have a rejection handler and are rejected will lead to an error in the console. Here is an example: 240 | ```lua 241 | d = deferred.new() 242 | d:reject("oh no!") 243 | -- Unhandled rejection: oh no! 244 | -- stack traceback: 245 | -- path/to/deferred.lua:187: in function '_handle' 246 | -- path/to/deferred.lua:46: in function 'reject' 247 | -- lua_run:1: in main chunk 248 | ``` 249 | Note that the first two lines of the traceback can be ignored. If you wish to disable this feature, add 250 | ```lua 251 | DEBUG_IGNOREUNHANDLED = true 252 | ``` 253 | to your code before using any of the functions. 254 | 255 | ## Testing 256 | To test, use `lua_openscript path/to/deferred_test.lua` in your console. Or, you can include the `deferred_test.lua` file directly. 257 | 258 | ## Credits 259 | Inspired by [zserge's lua-promises](https://github.com/zserge/lua-promises/). 260 | -------------------------------------------------------------------------------- /deferred.lua: -------------------------------------------------------------------------------- 1 | local PENDING, FULFILLED, REJECTED = "pending", "fulfilled", "rejected" 2 | local HANDLER_RESOLVE, HANDLER_REJECT, HANDLER_PROMISE = 1, 2, 3 3 | 4 | REJECTION_HANDLER_ID = REJECTION_HANDLER_ID or 0 5 | UNHANDLED_PROMISES = UNHANDLED_PROMISES or {} 6 | 7 | local Promise = { 8 | state = PENDING, 9 | value = nil, 10 | } 11 | Promise.__index = Promise 12 | 13 | function Promise:new() 14 | local instance = { 15 | onResolve = onResolve, 16 | onReject = onReject, 17 | handlers = {} 18 | } 19 | setmetatable(instance, Promise) 20 | return instance 21 | end 22 | 23 | function Promise:__tostring() 24 | local value = "" 25 | if (self.value) then 26 | value = ", value="..tostring(self.value) 27 | elseif (self.reason) then 28 | value = ", reason="..tostring(self.reason) 29 | end 30 | return "Promise{state="..self.state..value.."}" 31 | end 32 | 33 | function Promise:resolve(value) 34 | if (self.state == PENDING) then 35 | self.state = FULFILLED 36 | self.value = value 37 | self:_handle(value) 38 | end 39 | return self 40 | end 41 | 42 | function Promise:reject(reason) 43 | if (self.state == PENDING) then 44 | self.state = REJECTED 45 | self.reason = reason 46 | self:_handle(reason) 47 | end 48 | return self 49 | end 50 | 51 | function Promise:next(onResolve, onReject) 52 | -- Ignore an argument if it is not a function. 53 | if (not isfunction(onResolve)) then onResolve = nil end 54 | if (not isfunction(onReject)) then onReject = nil end 55 | 56 | local promise = Promise:new() 57 | self.handlers[#self.handlers + 1] = { 58 | [HANDLER_RESOLVE] = onResolve, 59 | [HANDLER_REJECT] = onReject, 60 | [HANDLER_PROMISE] = promise 61 | } 62 | 63 | if (self.state ~= PENDING) then 64 | timer.Simple(0, function() 65 | if (self.state == FULFILLED) then 66 | self:_handle(self.value) 67 | else 68 | self:_handle(self.reason) 69 | end 70 | end) 71 | end 72 | 73 | if (DEBUG_IGNOREUNHANDLED) then 74 | return promise 75 | end 76 | 77 | if (self.rejectionHandlerID) then 78 | promise.rejectionHandlerID = self.rejectionHandlerID 79 | else 80 | promise.rejectionHandlerID = REJECTION_HANDLER_ID 81 | UNHANDLED_PROMISES[REJECTION_HANDLER_ID] = true 82 | REJECTION_HANDLER_ID = REJECTION_HANDLER_ID + 1 83 | end 84 | return promise 85 | end 86 | 87 | function Promise:catch(onReject) 88 | return self:next(nil, onReject) 89 | end 90 | 91 | function Promise:_handle(value) 92 | -- Do not allow promises to resolve to themselves. 93 | if (value == self) then 94 | return self:reject("cannot resolve to self") 95 | end 96 | 97 | -- Adopt state if value is a promise. 98 | if (istable(value) and value.next) then 99 | if (value.state) then 100 | -- Adopt the rejection handler ID. 101 | if (not DEBUG_IGNOREUNHANDLED) then 102 | UNHANDLED_PROMISES[value.rejectionHandlerID] = nil 103 | value.rejectionHandlerID = self.rejectionHandlerID 104 | end 105 | 106 | -- Handle resolving to a promise. 107 | self.state = value.state 108 | if (value.state == PENDING) then 109 | self.value = value.value 110 | self.reason = value.reason 111 | value:next(function(newValue) 112 | self:resolve(newValue) 113 | return newValue 114 | end, function(reason) 115 | self:reject(reason) 116 | value.rejectionHandlerID = nil 117 | return reason 118 | end) 119 | elseif (value.state == FULFILLED) then 120 | self:_handle(value.value) 121 | else 122 | self:reject(value.reason) 123 | end 124 | return 125 | elseif (isfunction(value.next)) then 126 | -- Handle resolving to a thenable. 127 | self.state = PENDING 128 | self.value = nil 129 | local first = true 130 | local function resolvePromise(newValue) 131 | if (first) then 132 | self:resolve(newValue) 133 | first = nil 134 | end 135 | end 136 | local function rejectPromise(reason) 137 | if (first) then 138 | self:reject(reason) 139 | first = nil 140 | end 141 | end 142 | local status, result = 143 | pcall(value.next, resolvePromise, rejectPromise) 144 | if (not status and first) then 145 | self:reject(result) 146 | end 147 | return 148 | end 149 | end 150 | 151 | -- If value is not special, just resolve normally. 152 | local handler, onResolve, onReject, promise 153 | local isRejected = self.state == REJECTED 154 | 155 | for i = 1, #self.handlers do 156 | handler = self.handlers[i] 157 | onResolve = handler[HANDLER_RESOLVE] 158 | onReject = handler[HANDLER_REJECT] 159 | promise = handler[HANDLER_PROMISE] 160 | 161 | if (isRejected) then 162 | if (onReject) then 163 | local status, result = pcall(onReject, value) 164 | if (status) then 165 | promise:_handle(result) 166 | 167 | if (self.rejectionHandlerID) then 168 | UNHANDLED_PROMISES[self.rejectionHandlerID] = nil 169 | end 170 | else 171 | promise:reject(result) 172 | end 173 | else 174 | promise:reject(value) 175 | end 176 | else 177 | if (onResolve) then 178 | local status, result = pcall(onResolve, value) 179 | if (status) then 180 | promise:_handle(result) 181 | else 182 | promise:reject(result) 183 | end 184 | else 185 | promise:resolve(value) 186 | end 187 | end 188 | end 189 | self.handlers = {} 190 | if (isRejected and not DEBUG_IGNOREUNHANDLED) then 191 | local trace = debug.traceback() 192 | timer.Simple(0.1, function() 193 | if ( 194 | UNHANDLED_PROMISES[self.rejectionHandlerID] and 195 | not DEBUG_IGNOREUNHANDLED 196 | ) then 197 | UNHANDLED_PROMISES[self.rejectionHandlerID] = nil 198 | ErrorNoHalt( 199 | "Unhandled rejection: "..(self.reason or "").."\n" 200 | ) 201 | print(trace) 202 | end 203 | end) 204 | end 205 | end 206 | 207 | deferred = {} 208 | 209 | function deferred.isPromise(value) 210 | return istable(value) 211 | and isfunction(value.next) 212 | and isfunction(value.resolve) 213 | and value.state 214 | end 215 | 216 | function deferred.new() 217 | local promise = Promise:new() 218 | 219 | -- Bookkeeping for unhandled promises. 220 | if (not DEBUG_IGNOREUNHANDLED) then 221 | promise.rejectionHandlerID = REJECTION_HANDLER_ID 222 | UNHANDLED_PROMISES[REJECTION_HANDLER_ID] = true 223 | REJECTION_HANDLER_ID = REJECTION_HANDLER_ID + 1 224 | end 225 | 226 | return promise 227 | end 228 | 229 | function deferred.reject(reason) 230 | return deferred.new():reject(reason) 231 | end 232 | 233 | function deferred.resolve(value) 234 | return deferred.new():resolve(value) 235 | end 236 | 237 | function deferred.all(promises) 238 | assert(istable(promises), "promises must be a table of promises") 239 | local results = {} 240 | local d = deferred.new() 241 | local method = "resolve" 242 | local expected = #promises 243 | local finished = 0 244 | 245 | if (finished == expected) then 246 | return d:resolve(results) 247 | end 248 | 249 | local onFinish = function(i, resolved) 250 | return function(value) 251 | results[i] = value 252 | if (not resolved) then 253 | method = "reject" 254 | end 255 | finished = finished + 1 256 | if (finished == expected) then 257 | d[method](d, results) 258 | end 259 | return value 260 | end 261 | end 262 | 263 | for i = 1, expected do 264 | promises[i]:next(onFinish(i, true), onFinish(i, false)) 265 | end 266 | 267 | return d 268 | end 269 | 270 | function deferred.map(args, fn) 271 | assert(istable(args), "args must be a table of values") 272 | assert(isfunction(fn), "map called without a function") 273 | 274 | local expected = #args 275 | local finished = 0 276 | local results = {} 277 | local d = deferred.new() 278 | 279 | if (expected == 0) then 280 | return d:resolve(results) 281 | end 282 | 283 | for i = 1, expected do 284 | fn(args[i], i, expected):next(function(value) 285 | results[i] = value 286 | finished = finished + 1 287 | 288 | if (finished == expected) then 289 | d:resolve(results) 290 | end 291 | end, function(reason) 292 | d:reject(reason) 293 | end) 294 | end 295 | 296 | return d 297 | end 298 | 299 | function deferred.fold(promises, folder, initial) 300 | assert(istable(promises), "promises must be a table") 301 | assert(isfunction(folder), "folder must be a function") 302 | 303 | local d = deferred.new() 304 | local total = initial 305 | local length = #promises 306 | 307 | if (length == 0) then 308 | return d:resolve(total) 309 | end 310 | 311 | local i = 1 312 | 313 | local function onRejected(reason) 314 | d:reject(reason) 315 | return reason 316 | end 317 | 318 | local function handle(value) 319 | total = folder(total, value, i, length) 320 | 321 | if (i == length) then 322 | d:resolve(total) 323 | return value 324 | end 325 | 326 | i = i + 1 327 | promises[i]:next(handle, onRejected) 328 | return value 329 | end 330 | 331 | promises[1]:next(handle, onRejected) 332 | return d 333 | end 334 | 335 | function deferred.filter(promises, filter) 336 | return deferred.fold(promises, function(acc, value) 337 | if (filter(value)) then 338 | acc[#acc + 1] = value 339 | end 340 | return acc 341 | end, {}) 342 | end 343 | 344 | function deferred.each(promises, fn) 345 | return deferred.fold(promises, function(_, value, i, length) 346 | -- Ignore return value. 347 | fn(value, i, length) 348 | end, nil):next(function() 349 | -- Clear the return value. 350 | return nil 351 | end) 352 | end 353 | 354 | function deferred.some(promises, count) 355 | assert(istable(promises), "promises must be a table") 356 | assert( 357 | isnumber(count) and count >= 0 and math.floor(count) == count, 358 | "count must be a non-negative integer" 359 | ) 360 | 361 | local d = deferred.new() 362 | local results = {} 363 | local finished = 0 364 | if (count == finished) then 365 | return d:resolve(results) 366 | end 367 | 368 | for _, promise in ipairs(promises) do 369 | promise:next(function(value) 370 | if (d.state ~= PENDING) then return value end 371 | 372 | finished = finished + 1 373 | results[finished] = value 374 | if (finished == count) then 375 | d:resolve(results) 376 | end 377 | return value 378 | end, function(reason) 379 | d:reject(reason) 380 | end) 381 | end 382 | 383 | return d 384 | end 385 | 386 | function deferred.any(promises) 387 | return deferred.some(promises, 1) 388 | :next(function(results) 389 | return results[1] 390 | end) 391 | end 392 | -------------------------------------------------------------------------------- /deferred_test.lua: -------------------------------------------------------------------------------- 1 | include("deferred.lua") 2 | 3 | local tests = { 4 | -- 2.1.1.1: may transition to either the fulfilled or rejected state 5 | function() 6 | local d = deferred.new() 7 | assert(d.state == "pending") 8 | d:resolve(123) 9 | assert(d.state == "fulfilled") 10 | 11 | d = deferred.new() 12 | assert(d.state == "pending") 13 | d:reject(123) 14 | assert(d.state == "rejected") 15 | end, 16 | 17 | -- 2.1.2.1: when fulfilled, must not transition to any other state. 18 | function() 19 | local d = deferred.new() 20 | d:resolve(123) 21 | assert(d.state == "fulfilled") 22 | d:reject(456) 23 | assert(d.state == "fulfilled") 24 | end, 25 | 26 | -- 2.1.2.2: must have a value, which must not change. 27 | function() 28 | local d = deferred.new() 29 | d:resolve(123) 30 | assert(d.value == 123) 31 | d:resolve(456) 32 | assert(d.value == 123, "d.value = "..tostring(d.value).." != 123") 33 | end, 34 | 35 | -- 2.1.3.1: when rejected, must not transition to any other state. 36 | function() 37 | local d = deferred.new() 38 | d:reject(123) 39 | assert(d.state == "rejected") 40 | d:resolve(456) 41 | assert(d.state == "rejected") 42 | end, 43 | 44 | -- 2.1.3.2: must have a reason, which must not change. 45 | function() 46 | local d = deferred.new() 47 | d:reject(123) 48 | assert(d.reason == 123) 49 | d:reject(456) 50 | assert(d.reason == 123) 51 | end, 52 | 53 | -- 2.2.1: Both onFulfilled and onRejected are optional arguments 54 | function() 55 | local d = deferred.new() 56 | d:next() 57 | d:resolve(123) 58 | end, 59 | 60 | -- 2.2.1.1: If onFulfilled is not a function, it must be ignored. 61 | function() 62 | local d = deferred.new() 63 | d:next(123) 64 | d:resolve(123) 65 | end, 66 | 67 | -- 2.2.1.2: If onRejected is not a function, it must be ignored. 68 | function() 69 | local d = deferred.new() 70 | d:next(function() end, 123) 71 | d:resolve(123) 72 | 73 | d = deferred.new() 74 | d:next(nil, 123) 75 | d:resolve(123) 76 | 77 | d = deferred.new() 78 | d:next("wow", 123) 79 | d:resolve(123) 80 | end, 81 | 82 | -- 83 | -- 2.2.2. If onFulfilled is a function: 84 | -- 85 | -- 2.2.2.1/2: it must be called after promise is fulfilled, with promise’s 86 | -- value as its first argument 87 | function() 88 | local d = deferred.new() 89 | d:next(function(v) 90 | assert(d.state == "fulfilled", "onFulfilled called before fulfilled") 91 | assert(v == 123) 92 | end) 93 | d:resolve(123) 94 | end, 95 | 96 | -- 2.2.2.3: it must not be called more than once. 97 | function() 98 | local once = true 99 | local d = deferred.new() 100 | d:next(function() 101 | assert(once, "onFulfilled called more than once") 102 | once = false 103 | end) 104 | d:resolve(123) 105 | d:resolve(123) 106 | end, 107 | 108 | -- 109 | -- 2.2.6: then may be called multiple times on the same promise. 110 | -- 111 | -- 2.2.6.1: If/when promise is fulfilled, all respective onFulfilled 112 | -- callbacks must execute in the order of their originating calls to then 113 | function(wait) 114 | local i = 0 115 | local d = deferred.new() 116 | for j = 0, 9 do 117 | d:next(function() 118 | assert(i == j) 119 | i = i + 1 120 | end) 121 | end 122 | d:resolve() 123 | wait(1, function() 124 | assert(i == 10, "onFulfill ran "..i.." times, not 10") 125 | end) 126 | end, 127 | 128 | -- 2.2.6.2 If/when promise is rejected, all respective onRejected callbacks 129 | -- must execute in the order of their originating calls to then. 130 | function(wait) 131 | local i = 0 132 | local d = deferred.new() 133 | for j = 0, 9 do 134 | d:next(j, function() 135 | assert(i == j) 136 | i = i + 1 137 | end) 138 | end 139 | d:reject() 140 | wait(1, function() 141 | assert(i == 10, "onReject ran "..i.." times, not 10") 142 | end) 143 | end, 144 | 145 | -- 146 | -- 2.2.7: `then` must return a promise: 147 | -- `promise2 = promise1.then(onFulfilled, onRejected)` 148 | -- 149 | function() 150 | local d = deferred.new() 151 | local promise = d:next() 152 | assert(istable(promise), "return value of next is not a table") 153 | assert(isfunction(promise.next), "returned value has no next method") 154 | end, 155 | 156 | -- 2.2.7.2: If either `onFulfilled` or `onRejected` throws an exception 157 | -- `e`, `promise2` must be rejected with `e` as reason 158 | function(wait) 159 | local handled = false 160 | local d = deferred.new() 161 | d:next(function() 162 | error("no!") 163 | end) 164 | :next(nil, function(reason) 165 | assert(reason:find("no!")) 166 | handled = true 167 | end) 168 | d:resolve() 169 | wait(1, function() 170 | assert(handled, "promise2 did have no! inside reason") 171 | end) 172 | 173 | local handled2 = false 174 | d = deferred.new() 175 | d:next(nil, function() 176 | error("no!") 177 | end) 178 | :next(nil, function(err) 179 | assert(err:find("no!")) 180 | handled2 = true 181 | end) 182 | d:reject() 183 | wait(1, function() 184 | assert(handled2, "promise2 (#2) did not get handled!") 185 | end) 186 | end, 187 | 188 | -- 2.2.7.2: If either `onFulfilled` or `onRejected` throws an exception `e`, 189 | -- `promise2` must be rejected with `e` as the reason 190 | function() 191 | local d = deferred.new() 192 | d:next(function() 193 | error("test") 194 | end):next(function() 195 | assert(false, "should not be resolved") 196 | end, function(err) 197 | assert(err:find("test"), "rejected with incorrect reason") 198 | end) 199 | d:resolve() 200 | 201 | d = deferred.new() 202 | d:next(nil, function() 203 | error("test") 204 | end):next(function() 205 | assert(false, "should not be resolved") 206 | end, function(err) 207 | assert(err:find("test"), "rejected with incorrect reason") 208 | end) 209 | d:reject("wow") 210 | end, 211 | 212 | -- 2.2.7.3: If `onFulfilled` is not a function and `promise1` is fulfilled, 213 | -- `promise2` must be fulfilled with the same value 214 | function() 215 | local function testResolve(resolveValue) 216 | local ran = false 217 | local d = deferred.new() 218 | d:next(resolveValue):next(function(value) 219 | assert(value == resolveValue) 220 | ran = true 221 | end) 222 | d:resolve(resolveValue) 223 | timer.Simple(1, function() 224 | assert(ran, "promise2 onFulfilled not ran") 225 | end) 226 | end 227 | 228 | testResolve(nil) 229 | testResolve(false) 230 | testResolve(123) 231 | testResolve({a = 1, b = 2, c = 3}) 232 | testResolve({foo = function() 233 | local x = 1 234 | x = x + 1 235 | end}) 236 | end, 237 | 238 | -- 2.2.7.4: If `onRejected` is not a function and `promise1` is rejected, 239 | -- `promise2` must be rejected with the same reason 240 | function() 241 | local nonFnValues = {123, true, "wow", nil, function() end} 242 | nonFnValues[#nonFnValues + 1] = nonFnValues 243 | 244 | for _, value in ipairs(nonFnValues) do 245 | local d = deferred.new() 246 | d:next(nil, value):next(nil, function(reason) 247 | assert(reason == "what") 248 | end) 249 | d:reject("what") 250 | end 251 | end, 252 | 253 | -- 254 | -- 2.3: Promise Resolution Procedure [[Resolve]](promise, x) 255 | -- 256 | 257 | -- 2.3.1: If `promise` and `x` refer to the same object, reject `promise` 258 | function(wait) 259 | local d = deferred.new() 260 | d:resolve(d) 261 | wait(1, function() 262 | assert(d.state == "rejected") 263 | end) 264 | 265 | d = deferred.new() 266 | local promise = d:next(nil, function() 267 | return d 268 | end) 269 | promise:next(nil, function() 270 | assert(promise.state == "rejected") 271 | end) 272 | d:reject("what") 273 | end, 274 | 275 | -- 276 | -- 2.3.2: If x is a promise, adopt its state 277 | -- 278 | -- 2.3.2.1: If `x` is pending, `promise` must remain pending until `x` is 279 | -- fulfilled or rejected. 280 | function(wait) 281 | local d = deferred.new() 282 | local d2 = deferred.new() 283 | local result 284 | d:next(function(value) 285 | result = value 286 | end) 287 | d:resolve(d2) 288 | assert(d.state == "pending") 289 | wait(0.5, function() 290 | assert(d.state == "pending", "adopted state not the same") 291 | d2:resolve(123) 292 | end) 293 | wait(1, function() 294 | assert(d.state == "fulfilled", "state not adopted") 295 | end) 296 | end, 297 | function(wait) 298 | local d = deferred.new() 299 | local d2 = deferred.new() 300 | local result 301 | d:next(nil, function(value) 302 | result = value 303 | end) 304 | d:resolve(d2) 305 | assert(d.state == "pending") 306 | wait(0.5, function() 307 | assert(d.state == "pending", "adopted state not the same") 308 | d2:reject(123) 309 | end) 310 | wait(1, function() 311 | assert(d.state == "rejected", "state not adopted") 312 | end) 313 | end, 314 | 315 | -- 2.3.2.2: If/when `x` is fulfilled, fulfill `promise` with the same value. 316 | function(wait) 317 | local d = deferred.new() 318 | local d2 = deferred.new() 319 | local result = nil 320 | d:next(function(value) 321 | result = value 322 | end) 323 | d:resolve(d2) 324 | assert(d.value == nil, "d should not have a value") 325 | assert(result == nil, "result should not be set") 326 | wait(0.5, function() 327 | assert(d.value == nil, "d should not have a value") 328 | assert(result == nil, "result should not be set") 329 | d2:resolve(123) 330 | end) 331 | wait(1, function() 332 | assert(result == 123, "value not adopted") 333 | end) 334 | end, 335 | function() 336 | local d = deferred.new() 337 | local d2 = deferred.new() 338 | d2:resolve(123) 339 | d:next(function(value) 340 | assert(value == 123, "pre-resolved value is not 123") 341 | end) 342 | d:resolve(d2) 343 | end, 344 | 345 | -- 2.3.2.3: If/when `x` is rejected, reject `promise` with the same reason 346 | function(wait) 347 | local d = deferred.new() 348 | local d2 = deferred.new() 349 | local result 350 | d:next(nil, function(value) 351 | result = value 352 | end) 353 | d:resolve(d2) 354 | assert(d.value == nil, "d should not have a value") 355 | assert(result == nil, "result should not be set") 356 | wait(0.5, function() 357 | assert(d.value == nil, "d should not have a value") 358 | assert(result == nil, "result should not be set") 359 | d2:reject(123) 360 | end) 361 | wait(1, function() 362 | assert(result == 123, "reason not adopted") 363 | end) 364 | end, 365 | function() 366 | local d = deferred.new() 367 | local d2 = deferred.new() 368 | d2:reject(123) 369 | d:next(nil, function(value) 370 | assert(value == 123, "pre-resolved value is not 123") 371 | end) 372 | d:resolve(d2) 373 | end, 374 | 375 | -- 376 | -- 2.3.3 Otherwise, if x is an object or function 377 | -- Let `next` be `x.next` 378 | -- 379 | 380 | -- 2.3.3.3: If `next` is a function, call it with first 381 | -- argument `resolvePromise`, and second argument `rejectPromise` 382 | function(wait) 383 | local d = deferred.new() 384 | local okay = false 385 | d:next(function() 386 | return { 387 | next = function(resolve, reject) 388 | assert(isfunction(resolve), "resolve must be a function") 389 | assert(isfunction(reject), "reject must be a function") 390 | okay = true 391 | end 392 | } 393 | end) 394 | d:resolve() 395 | wait(1, function() assert(okay, "d did not get handled") end) 396 | end, 397 | 398 | -- 2.3.3.3.1: If/when `resolvePromise` is called with a value `y`, 399 | -- run [[Resolve]](promise, y) 400 | 401 | -- `y` is not thenable 402 | function(wait) 403 | local passed = true 404 | local nonThenables = {false, 123.4, "hello world", {a = 1, b = 2}} 405 | local resolvers = {} 406 | for k, v in ipairs(nonThenables) do 407 | resolvers[k] = function() 408 | return { 409 | next = function(resolve) resolve(v) end 410 | } 411 | end 412 | end 413 | 414 | for k, resolver in ipairs(resolvers) do 415 | local d = deferred.new() 416 | d:next(resolver):next(function(value) 417 | if (value ~= nonThenables[k]) then 418 | print("Expected "..tostring(nonThenables[k])..", got "..tostring(value)) 419 | passed = false 420 | end 421 | end) 422 | d:resolve() 423 | end 424 | 425 | wait(1, function() 426 | assert(passed, "non-thenable resolution failed") 427 | end) 428 | end, 429 | -- `y` is thenable 430 | function(wait) 431 | local passed = true 432 | local nonThenables = {false, 123.4, "hello world", {a = 1, b = 2}} 433 | local thenables = {} 434 | for k, v in ipairs(nonThenables) do 435 | thenables[k] = function() 436 | return { 437 | next = function(resolve, reject) 438 | resolve(v) 439 | end 440 | } 441 | end 442 | end 443 | 444 | for k, thenable in ipairs(thenables) do 445 | local d = deferred.new() 446 | d:next(thenable):next(function(value) 447 | if (value ~= nonThenables[k]) then 448 | print("Expected "..tostring(nonThenables[k])..", got "..tostring(value)) 449 | passed = false 450 | end 451 | end) 452 | d:resolve() 453 | end 454 | 455 | wait(1, function() 456 | assert(passed, "thenable resolution failed") 457 | end) 458 | end, 459 | -- `y` is a promise 460 | function(wait) 461 | local matched = 0 462 | local nonThenables = {false, 123.4, "hello world", {a = 1, b = 2}} 463 | local promises = {} 464 | for k, v in ipairs(nonThenables) do 465 | promises[k] = function() 466 | return { 467 | next = function(resolve) 468 | local d = deferred.new() 469 | wait(0.5, function() 470 | d:resolve(v) 471 | end) 472 | resolve(d) 473 | end 474 | } 475 | end 476 | end 477 | 478 | for k, promise in ipairs(promises) do 479 | local d = deferred.new() 480 | d:next(promise):next(function(value) 481 | if (value == nonThenables[k]) then 482 | matched = matched + 1 483 | else 484 | print("Expected "..tostring(nonThenables[k])..", got "..tostring(value)) 485 | end 486 | end) 487 | d:resolve() 488 | end 489 | 490 | wait(1, function() 491 | assert(matched == #promises, "promise resolution failed") 492 | end) 493 | end, 494 | 495 | -- 2.3.3.3.2: If/when rejectPromise is called with a reason `r`, 496 | -- reject promise with `r` 497 | function(wait) 498 | local ran = false 499 | local d = deferred.new() 500 | d:next(function() 501 | return { 502 | next = function(_, reject) 503 | reject("dang") 504 | end 505 | } 506 | end) 507 | :next(nil, function(reason) 508 | assert(reason:find("dang"), "incorrect reason") 509 | ran = true 510 | end) 511 | d:resolve() 512 | 513 | wait(1, function() 514 | assert(ran, "incorrect reason or did not run") 515 | end) 516 | end, 517 | 518 | -- 2.3.3.3.3: If both resolvePromise and rejectPromise are called, or 519 | -- multiple calls to the same argument are made, the first call takes 520 | -- precedence, and any further calls are ignored. 521 | function(wait) 522 | -- Resolve, then reject 523 | local d0 = deferred.new() 524 | local p0 = d0:next(function() 525 | return { 526 | next = function(resolve, reject) 527 | resolve(123) 528 | reject(456) 529 | end 530 | } 531 | end) 532 | d0:resolve() 533 | wait(1, function() 534 | assert(p0.state == "fulfilled", "p0 not fulfilled") 535 | assert(p0.value == 123, "p0's value is incorrect") 536 | end) 537 | 538 | -- Reject, then resolve 539 | local d1 = deferred.new() 540 | local finalValue1 541 | local p1 = d1:next(function() 542 | return { 543 | next = function(resolve, reject) 544 | reject(123) 545 | resolve(456) 546 | end 547 | } 548 | end) 549 | d1:resolve() 550 | wait(1, function() 551 | assert(p1.state == "rejected", "p1 not rejected") 552 | assert(p1.reason == 123, "p1's reason is incorrect") 553 | end) 554 | 555 | -- Resolve multiple times 556 | local d2 = deferred.new() 557 | local p2 = d2:next(function() 558 | return { 559 | next = function(resolve, reject) 560 | for i = 10, 1, -1 do 561 | resolve(i) 562 | end 563 | end 564 | } 565 | end) 566 | d2:resolve() 567 | wait(1, function() 568 | assert(p2.state == "fulfilled", "p2 not fulfilled") 569 | assert(p2.value == 10, "p2's value is not that of first resolve") 570 | end) 571 | 572 | -- Reject multiple times 573 | local d3 = deferred.new() 574 | local p3 = d3:next(function() 575 | return { 576 | next = function(resolve, reject) 577 | for i = 10, 1, -1 do 578 | reject(i) 579 | end 580 | end 581 | } 582 | end) 583 | d3:resolve() 584 | wait(1, function() 585 | assert(p3.state == "rejected", "p3 not rejected") 586 | assert(p3.reason == 10, "p3's value is not that of first reject") 587 | end) 588 | end, 589 | 590 | -- 591 | -- 2.3.3.3.4: If calling then throws an exception `e` 592 | -- 593 | -- 2.3.3.3.4.1: If `resolvePromise` or `rejectPromise` have been called, 594 | -- ignore it. 595 | function(wait) 596 | local d = deferred.new() 597 | local p = d:next(function() 598 | return { 599 | next = function(resolve) 600 | resolve(123) 601 | error("noo!") 602 | end 603 | } 604 | end) 605 | d:resolve() 606 | wait(1, function() 607 | assert(p.state == "fulfilled", "p is not fulfilled") 608 | assert(p.value == 123, "p.value = "..tostring(p.value).." ~= 123") 609 | end) 610 | end, 611 | function(wait) 612 | local d = deferred.new() 613 | local p = d:next(function() 614 | return { 615 | next = function(_, reject) 616 | reject(123) 617 | error("noo!") 618 | end 619 | } 620 | end) 621 | d:resolve() 622 | wait(1, function() 623 | assert(p.state == "rejected", "p is not rejected") 624 | assert(p.reason == 123, "p.reason = "..tostring(p.value).." ~= 123") 625 | end) 626 | end, 627 | 628 | -- 2.3.3.4: If `next` is not a function, fulfill promise with `x` 629 | function(wait) 630 | local function testResolve(value) 631 | local ran = false 632 | local d = deferred.new() 633 | local testValue = {next = value} 634 | d:next(function() 635 | return testValue 636 | end):next(function(value2) 637 | assert(testValue == value2, tostring(testValue).." != "..tostring(value2)) 638 | ran = true 639 | end) 640 | d:resolve() 641 | wait(1, function() 642 | assert(ran, "did not get "..tostring(testValue).." back") 643 | end) 644 | end 645 | 646 | testResolve(123) 647 | testResolve(true) 648 | testResolve(false) 649 | testResolve(nil) 650 | testResolve({a = 1, b = 2, c = 3, d = "hello world"}) 651 | testResolve("hello world") 652 | testResolve(deferred.new()) 653 | end, 654 | 655 | -- 2.3.4: If `x` is not an object or function, fulfill promise with `x`. 656 | function(wait) 657 | local function testResolve(value) 658 | local ran = false 659 | local d = deferred.new() 660 | d:next(function(value2) 661 | assert(value == value2, "value mismatch with "..tostring(value)) 662 | ran = true 663 | end) 664 | wait(1, function() 665 | assert(ran, "did not get "..tostring(value).." back") 666 | end) 667 | d:resolve(value) 668 | end 669 | 670 | testResolve(123) 671 | testResolve(true) 672 | testResolve(false) 673 | testResolve(nil) 674 | testResolve({a = 1, b = 2, c = 3, d = "hello world"}) 675 | testResolve("hello world") 676 | end, 677 | } 678 | 679 | local ran = 0 680 | local passed = 0 681 | local expected = #tests 682 | 683 | local function finishTests() 684 | print("*********************************************************") 685 | local count = passed.."/"..expected 686 | if (passed < expected) then 687 | MsgC(Color(255, 0, 0), count) 688 | else 689 | MsgC(Color(0, 255, 0), count) 690 | end 691 | MsgN(" TESTS PASSED") 692 | print("*********************************************************") 693 | end 694 | 695 | DEBUG_IGNOREUNHANDLED = true 696 | 697 | for i, test in ipairs(tests) do 698 | local function done(testPassed, err) 699 | ran = ran + 1 700 | passed = passed + (testPassed and 1 or 0) 701 | 702 | if (not testPassed) then 703 | MsgC(Color(255, 0, 0), "X ") 704 | print("TEST #"..i.." FAILED!") 705 | print("\t"..err) 706 | end 707 | 708 | if (ran == expected) then 709 | finishTests() 710 | DEBUG_IGNOREUNHANDLED = false 711 | end 712 | end 713 | 714 | local defer = false 715 | local function wait(time, callback) 716 | if (defer) then 717 | expected = expected + 1 718 | end 719 | defer = true 720 | timer.Simple(time, function() 721 | done(pcall(callback)) 722 | end) 723 | end 724 | 725 | local status, result = pcall(test, wait) 726 | if (not defer) then 727 | done(status, result) 728 | end 729 | end --------------------------------------------------------------------------------