└── addons └── gdscript-async-utils ├── plugin.cfg ├── promise.gd └── promise.ico /addons/gdscript-async-utils/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GDScript Async Utils" 4 | description="Provide async functionality to have Promises similar to JavaScript with functions like Promise.all, Promise.race, Promise.any" 5 | author="Mateo Miccino" 6 | version="0.2" 7 | script="Promise.gd" -------------------------------------------------------------------------------- /addons/gdscript-async-utils/promise.gd: -------------------------------------------------------------------------------- 1 | # Promises for GDScript 2 | # Every function that must be awaited has an `async_` prefix 3 | 4 | class_name Promise 5 | 6 | signal on_resolved 7 | 8 | var _resolved: bool = false 9 | var _data: Variant = null 10 | var _on_resolve_cbs: Array[Callable] = [] 11 | var _on_reject_cbs: Array[Callable] = [] 12 | 13 | 14 | func _valid_callback(callback: Callable) -> bool: 15 | if !callback.is_valid(): 16 | printerr("Invalid callback") 17 | return false 18 | 19 | if callback.get_bound_arguments_count() >= 2: 20 | printerr("Invalid arguments on callback") 21 | return false 22 | 23 | return true 24 | 25 | 26 | func then(on_resolved: Callable) -> Promise: 27 | if !_valid_callback(on_resolved): 28 | printerr("Invalid callback") 29 | return self 30 | 31 | if is_resolved() and !is_rejected(): 32 | on_resolved.call(_data) 33 | else: 34 | _on_resolve_cbs.push_back(on_resolved) 35 | 36 | return self 37 | 38 | 39 | func catch(on_rejected: Callable) -> Promise: 40 | if !_valid_callback(on_rejected): 41 | printerr("Invalid callback") 42 | return self 43 | 44 | if is_resolved() and is_rejected(): 45 | on_rejected.call(_data) 46 | else: 47 | _on_reject_cbs.push_back(on_rejected) 48 | 49 | return self 50 | 51 | 52 | func resolve(): 53 | resolve_with_data(null) 54 | 55 | 56 | func resolve_with_data(data): 57 | if is_resolved(): 58 | return 59 | _resolved = true 60 | _data = data 61 | 62 | if data is Promise.Error: 63 | for on_reject in _on_reject_cbs: 64 | if on_reject.is_valid(): 65 | on_reject.call(_data) 66 | else: 67 | for on_resolve in _on_resolve_cbs: 68 | if on_resolve.is_valid(): 69 | on_resolve.call(_data) 70 | 71 | on_resolved.emit() 72 | 73 | 74 | func get_data(): 75 | return _data 76 | 77 | 78 | func reject(reason: String): 79 | resolve_with_data(Promise.Error.create(reason)) 80 | 81 | 82 | func is_rejected() -> bool: 83 | return _data is Promise.Error 84 | 85 | 86 | func is_resolved() -> bool: 87 | return _resolved 88 | 89 | 90 | func async_awaiter() -> Variant: 91 | if !_resolved: 92 | await on_resolved 93 | if _data is Promise: # Chain promises 94 | return _data.async_awaiter() 95 | 96 | return _data 97 | 98 | 99 | class Error: 100 | var _error_description: String = "" 101 | 102 | static func create(description: String) -> Promise.Error: 103 | var error = Promise.Error.new() 104 | error._error_description = description 105 | return error 106 | 107 | func get_error() -> String: 108 | return _error_description 109 | 110 | 111 | # Internal helper function 112 | class _Internal: 113 | static func async_call_and_get_promise(f) -> Promise: 114 | if f is Promise: 115 | return f 116 | 117 | if f is Callable: 118 | var res = await f.call() 119 | if res is Promise: 120 | return res 121 | 122 | printerr("Func doesn't return a Promise") 123 | return null 124 | 125 | printerr("Func is not a callable nor promise") 126 | return null 127 | 128 | 129 | class AllAwaiter: 130 | var results: Array = [] 131 | var _mask: int 132 | var _promise: Promise = Promise.new() 133 | 134 | func _init(funcs: Array) -> void: 135 | var size := funcs.size() 136 | if size == 0: # inmediate resolve, no funcs to await... 137 | _promise.resolve() 138 | return 139 | 140 | results.resize(size) 141 | results.fill(null) # by default, the return will be null 142 | assert(size < 64) 143 | _mask = (1 << size) - 1 144 | for i in size: 145 | _async_call_func(i, funcs[i]) 146 | 147 | func _async_call_func(i: int, f) -> void: 148 | @warning_ignore("redundant_await") 149 | var promise = await Promise._Internal.async_call_and_get_promise(f) 150 | var data = await promise.async_awaiter() 151 | results[i] = data 152 | 153 | _mask &= ~(1 << i) 154 | 155 | if not _mask and not _promise.is_resolved(): 156 | _promise.resolve_with_data(results) 157 | 158 | 159 | class AnyAwaiter: 160 | var _promise: Promise = Promise.new() 161 | 162 | func _init(funcs: Array) -> void: 163 | var size := funcs.size() 164 | if size == 0: # inmediate resolve, no funcs to await... 165 | _promise.resolve() 166 | return 167 | for i in size: 168 | _async_call_func(i, funcs[i]) 169 | 170 | func _async_call_func(_i: int, f) -> void: 171 | @warning_ignore("redundant_await") 172 | var promise: Promise = await Promise._Internal.async_call_and_get_promise(f) 173 | var res = await promise.async_awaiter() 174 | 175 | # Promise.async_any ignores promises with errors 176 | if !promise.is_rejected() and not _promise.is_resolved(): 177 | _promise.resolve_with_data(res) 178 | 179 | 180 | class RaceAwaiter: 181 | var _promise: Promise = Promise.new() 182 | 183 | func _init(funcs: Array) -> void: 184 | var size := funcs.size() 185 | if size == 0: # inmediate resolve, no funcs to await... 186 | _promise.resolve() 187 | return 188 | for i in size: 189 | _async_call_func(i, funcs[i]) 190 | 191 | func _async_call_func(_i: int, f) -> void: 192 | @warning_ignore("redundant_await") 193 | var promise: Promise = await Promise._Internal.async_call_and_get_promise(f) 194 | var res = await promise.async_awaiter() 195 | 196 | # Promise.async_race doesn't ignore on error, you get the first one, with or without an error 197 | if not _promise.is_resolved(): 198 | _promise.resolve_with_data(res) 199 | 200 | 201 | # `async_all` is a static function that takes an array of functions (`funcs`) 202 | # and returns an array. It awaits the resolution of all the given functions. 203 | # Each function in the array is expected to be a coroutine or a function 204 | # that returns a promise. 205 | static func async_all(funcs: Array) -> Array: 206 | if funcs.is_empty(): 207 | return [] 208 | return await AllAwaiter.new(funcs)._promise.async_awaiter() 209 | 210 | 211 | # `async_any` is a static function similar to `async_all`, but it resolves as soon as any of the 212 | # functions in the provided array resolves. It returns the result of the first function 213 | # that resolves. It ignores the rejections (differently from async_race) 214 | static func async_any(funcs: Array) -> Variant: 215 | return await AnyAwaiter.new(funcs)._promise.async_awaiter() 216 | 217 | 218 | # `async_race` is another static function that takes an array of functions and returns 219 | # a variant. It behaves like a race condition, returning the result of the function 220 | # that completes first, even if it fails (differently from async_any) 221 | static func async_race(funcs: Array) -> Variant: 222 | return await RaceAwaiter.new(funcs)._promise.async_awaiter() 223 | -------------------------------------------------------------------------------- /addons/gdscript-async-utils/promise.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuruk-mm/gdscript-promise-async-utils/1866bc08acc9c11fb2464442c32b7f90df59df6f/addons/gdscript-async-utils/promise.ico --------------------------------------------------------------------------------