async-promise 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/changelog.md +48 -0
  3. data/lib/async/promise.rb +279 -0
  4. data/readme.md +35 -0
  5. metadata +122 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 763599aeac2aaa69d7c7d102d442d1bf684ab07f2d75fc6d51701eeb98c46724
4
+ data.tar.gz: faaf3deb61d7a62ded3d5173705838be52d783add35a5d4a6cb9c41caccf013a
5
+ SHA512:
6
+ metadata.gz: 3c8b6e6bdb75ea524ea3e686bbfe5d60d2732c05f28961285c720417c6e87bea6a1d8fb74c2cab55db13706eb0d92b60d360b28a451964b1881a57482bf32673
7
+ data.tar.gz: 00f1697f14ec5843ff253c78ea21faec67d52dca78fc98d4d629596398e1c89e03fb964037d0b887d910187da91ede9c93cb0cda02d4901a769b65cd9a24f52e
data/changelog.md ADDED
@@ -0,0 +1,48 @@
1
+ ## [Unreleased]
2
+
3
+
4
+ ## [0.1.0] - 2024-10-03
5
+
6
+ ### Summary
7
+ - Initial release
8
+
9
+ ### Added
10
+ - Ability to create Javascript ES6 like [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) via the `Async::Promise` class.
11
+ - Add the following `Promise` instance methods:
12
+ - `#status(): "pending" | "fulfilled" | "rejected"`
13
+ - get the status of a promise, which can either be `"pending"`, `"fulfilled"`, or `"rejected"`.
14
+ - when a promise is either `"fulfilled"` or `"rejected"`, we say that it has been *settled*.
15
+ - `#resolve(value?: T): void`
16
+ - resolve a `"pending"` promise with the given `value`, and set the `#status` of the promise to `"fulfilled"`.
17
+ - an already settled promise cannot be resolved nor rejected again.
18
+ - `#reject(reason?: String | StandardException): void`
19
+ - reject a `"pending"` promise with the given `reason`, and set the `#status` of the promise to `"rejected"`.
20
+ - an already settled promise cannot be resolved nor rejected again.
21
+ - `#then(on_resolve?: nil | ((value: T) => (V | Promise<V>)), on_reject?: nil | ((reason: String | StandardException) => (V | Promise<V>))): Promise<V>`
22
+ - chain the current promise with an `on_resolve` and an `on_reject` function, which shall be called when the current promise is resolved.
23
+ - the returned value is another promise, that is resolved once either the `on_resolve` or `on_reject` are ran successfully.
24
+ - when either the `on_resolve` or `on_reject` functions are `nil`, the supposed `value`/`reason` they are to receive will be passed onto the dependent promises of the returned promise.
25
+ in a way, it will behave as if `on_resolve = ->(value) { return value }` and `on_reject = ->(reason) { raise reason }`.
26
+ - `#catch(on_reject?: nil | ((reason: String | StandardException) => (V | Promise<V>)))`
27
+ - catch any raised exceptions that have occurred in preceding chain of promises.
28
+ - this is functionally equivalent to `some_promise.then(nil, ->(reason){ "take care of the error" })`
29
+ - `#wait(): T`
30
+ - wait for a promise to settle, similar to how one can await a promise in javascript via `await some_promise`.
31
+ - the returned value will be the resolved value of the promise (i.e. when the status is `"fulfilled"`), or it will raise an error if the promise was rejected (i.e. when the status is `"rejected"`).
32
+ - Add the following `Promise` class methods:
33
+ - `.resolve(value?: T): Promise<T>`
34
+ - creates an already resolved promise.
35
+ - `.reject(reason?: String | StandardException): Promise<T>`
36
+ - creates an already rejected promise.
37
+ - `.all(promises: Array<Promise<T>>): Promise<Array<T>>`
38
+ - create a new promise that resolves when all of its input promises have been resolved, and rejects when any single input promise is rejected.
39
+ - `.race(promises: Array<Promise<T>>): Promise<T>`
40
+ - create a new promise that either rejects or resolves based on whichever encompassing promise settles first.
41
+ - `.timeout(resolve_in: Float, reject_in: Float, resolve: T, reject: String, StandardError): Promise<T>`
42
+ - create a promise that either resolves or rejects after a given timeout.
43
+
44
+ ### Dependency
45
+ - Add dependency on the [async](https://github.com/socketry/async) gem.
46
+ More specifically, the library depends on the following two constructs of the said gem:
47
+ - `Async` Kernel block
48
+ - `Async::Variable` Class
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/variable"
5
+
6
+ module Async
7
+ # An Asynchronous Promise (of generic type `T`) holds necessary information about what should be executed when the promise is resolved or rejected,
8
+ # and which child Promise nodes to propagate the output values of the resolver/rejector to.
9
+ class Promise < Async::Variable
10
+ VERSION = "0.1.0"
11
+
12
+ alias_method :async_resolve, :resolve # rename the `Async::Variable.resolve` instance method to `async_resolve`, since we will be using the same method name for our own logic of resolving values.
13
+ alias_method :async_wait, :wait # rename the `Async::Variable.wait` instance method to `async_wait`, since we will need to tap into the waiting process to raise any errors that may have occurred during the process.
14
+
15
+ # @param on_resolve [(value) => next_value_or_promise, nil] the function to call when the promise is resolved with a value.
16
+ # @param on_reject [(reason) => next_value_or_promise, nil] the function to call when the promise is rejected (either manually or due to a raised error).
17
+ def initialize(on_resolve = nil, on_reject = nil)
18
+ super()
19
+ # @type ["pending", "fulfilled", "rejected"] Represents the current state of this Promise node.
20
+ @status = "pending"
21
+ # @type [Array<Promise>] An array of child [Promise] nodes that will be notified when this promise resolves.
22
+ # the resulting structure is a tree-like, and we shall traverse them in DFS (depth first search).
23
+ @children = []
24
+ # @type [Any] the value of type `T` that will be assigned to this node when it resolves.
25
+ # this value is kept purely for the sake of providing latecomer-then calls with a resolve value (if the `@status` was "fulfilled")
26
+ # a latecomer is when a call to the [then] or [catch] method is made after the promise has already been "fulfilled".
27
+ @value = nil
28
+ # @type [String, StandardError] the error reason that will be assigned to this node when it is rejected.
29
+ # this value is kept purely for the sake of providing latecomer-then calls with a rejection reason (if the `@status` was "rejected")
30
+ # a latecomer is when a call to the [then] or [catch] method is made after the promise has already been "rejected".
31
+ @reason = nil
32
+ @on_resolve = on_resolve
33
+ @on_reject = on_reject
34
+ end
35
+
36
+ # Create a new Promise that is already resolved with the provided [value] of type `T`.
37
+ # @param value [T, Promise<T>] Generic value of type `T`.
38
+ # return [Promise<T>] The newly created (and resolved) promise is returned.
39
+ # TODO: create unit test, and maybe reduce the amount of pre-resolved promises you create in your tests through the use of this
40
+ def self.resolve(value = nil)
41
+ new_promise = new()
42
+ new_promise.resolve(value)
43
+ new_promise
44
+ end
45
+
46
+ # Create a new Promise that is already rejected with the provided [reason].
47
+ # WARNING: Do not pass a `nil` as the reason, because it will break the error handling logic, since it will seem to it like there was no error.
48
+ # @param reason [String, StandardError] Give the reason for rejection.
49
+ # return [Promise<nil>] The newly created (and rejected) promise is returned.
50
+ # TODO: create unit test, and maybe reduce the amount of pre-resolved rejects you create in your tests through the use of this
51
+ def self.reject(reason = "Promise error")
52
+ new_promise = new()
53
+ new_promise.reject(reason)
54
+ new_promise
55
+ end
56
+
57
+ # Create a new Promise that resolves when all of its input promises have been resolved, and rejects when any single input promise is rejected.
58
+ # @param promises [Array<[T, Promise<T>]>] Provide all of the input T or Promise<T> to wait for, in order to resolve.
59
+ # return [Promise<Array<T>>] Returns a Promise which, when resolved, contains the array of all resolved values.
60
+ # TODO: create unit test
61
+ def self.all(promises = [])
62
+ # we must return early on if no promises we given, since nothing will then resolve the new_promise.
63
+ return self.resolve([]) if promises.empty?
64
+ # next, we make sure that all values within the `promises` array are actual `Promise`s. the ones that aren't, are converted to a resolved promise.
65
+ promises = promises.map { |p| p.is_a?(Promise) ? p : self.resolve(p) }
66
+ resolved_values = []
67
+ remaining_promises = promises.length
68
+ new_promise = new()
69
+
70
+ # The following may not be the prettiest implementation. TODO: consider if you can use a Array.map for this function
71
+ promises.each_with_index { |promise, index|
72
+ promise.then(->(value) {
73
+ resolved_values[index] = value
74
+ remaining_promises -= 1
75
+ if remaining_promises == 0
76
+ new_promise.resolve(resolved_values)
77
+ end
78
+ }, ->(reason) {
79
+ # if there is any rejected dependency promise, we should immediately reject our new_promise
80
+ # note that this is somewhat of a error-racing scenario, since the new promise will be rejected due to the first error it encounters.
81
+ # i.e. its order can vary from time to time, possibly resulting in different kinds of rejection reasons
82
+ new_promise.reject(reason)
83
+ })
84
+ }
85
+ new_promise
86
+ end
87
+
88
+ # TODO: implement `Promise.allSettled` static method
89
+
90
+ # Create a new Promise that either rejects or resolves based on whichever encompassing promise settles first.
91
+ # The value it either resolves or rejects with, will be inherited by the promise that wins the race.
92
+ # @param promises [Array<[T, Promise<T>]>] Provide all of the input Promise<T> to wait for, in order to resolve.
93
+ # return [Promise<Array<T>>] Returns a Promise which, when resolved, contains the array of all resolved values.
94
+ def self.race(promises)
95
+ # if any of the promises happens to be a regular value (i.e. not an Promise), then we will just return that (whichever one that we encounter first)
96
+ promises.each { |p|
97
+ unless p.is_a?(Promise)
98
+ return self.resolve(p)
99
+ end
100
+ }
101
+ new_promise = new()
102
+ # in the racing condition, there is no need to check if `new_promise` was already settled, because the `resolve` and `reject` methods already ignore requests made after the resolve/reject.
103
+ # thus it is ok for our each loop to mindlessly and rapidly call `new_promise.resolve` and `new_promise.reject` with no consequences (might affect performance? probably just a micro optimization).
104
+ promises.each { |promise|
105
+ promise.then(
106
+ ->(value) { new_promise.resolve(value) },
107
+ ->(reason) { new_promise.reject(reason) },
108
+ )
109
+ }
110
+ new_promise
111
+ end
112
+
113
+ # Create a promise that either resolves or rejects after a given timeout.
114
+ # @param resolve_in [Float, nil] The time in seconds to wait before resolving with keyword-value `resolve` (if provided).
115
+ # @param reject_in [Float, nil] The time in seconds to wait before rejecting with keyword-reason `reject` (if provided).
116
+ # @param resolve [Any] The value to resolve with after `resolve_in` seconds.
117
+ # @param reject [String, StandardError] The reason to reject after `reject_in` seconds.
118
+ def self.timeout(resolve_in = nil, reject_in = nil, resolve: nil, reject: "Promise timeout")
119
+ new_promise = new
120
+ # if both timers are `nil`, then we will just return a never resolving promise (at least not from here. but it can be resolved externally)
121
+ return new_promise if resolve_in.nil? && reject_in.nil?
122
+ Async do
123
+ # determine the shorter timeout and take the action of either resolving or rejecting after the timeout
124
+ if (reject_in.nil?) || ((not resolve_in.nil?) && (resolve_in <= reject_in))
125
+ sleep(resolve_in)
126
+ new_promise.resolve(resolve)
127
+ else
128
+ sleep(reject_in)
129
+ new_promise.reject(reject)
130
+ end
131
+ end
132
+ new_promise
133
+ end
134
+
135
+ # Resolve the value of this Promise node.
136
+ # @param value [T, Promise<T>] Generic value of type `T`.
137
+ # return [void] nothing is returned.
138
+ def resolve(value = nil)
139
+ return nil if @status != "pending" # if the promise is already fulfilled or rejected, return immediately
140
+
141
+ Async do |task|
142
+ if value.is_a?(Promise)
143
+ # if the provided value itself is a promise, then this (self) promise will need to become dependant on it.
144
+ value.then(
145
+ ->(resolved_value) { self.resolve(resolved_value); resolved_value },
146
+ ->(rejection_reason) { self.reject(rejection_reason); rejection_reason },
147
+ )
148
+ else
149
+ # otherwise, since we have an actual resolved value at hand, we may now pass it onto the dependent children.
150
+ begin
151
+ value = @on_resolve.nil? \
152
+ ? value
153
+ : @on_resolve.call(value) # it's ok if `@on_resolve` returns another promise object, because the children will then lach on to its promise when their `resolve` method is called.
154
+ rescue => error_reason
155
+ # some uncaught error occurred while running the `@on_resolve` function. we should now reject this (self) promise, and pass the responsibility of handling to the children (if any).
156
+ self.handle_imminent_reject(error_reason)
157
+ else
158
+ # no errors occurred after running the `@on_resolve` function. we may now resolve the children.
159
+ self.handle_imminent_resolve(value)
160
+ end
161
+ end
162
+ end
163
+
164
+ nil
165
+ end
166
+
167
+ # Reject the value of this Promise node with an optional error reason.
168
+ # WARNING: Do not pass a `nil` as the reason, because it will break the error handling logic, since it will seem to it like there was no error.
169
+ # @param reason [String, StandardError] The error to pass on to the next series of dependant promises.
170
+ # return [void] nothing is returned.
171
+ def reject(reason = "Promise error")
172
+ return nil if @status != "pending" # if the promise is already fulfilled or rejected, return immediately
173
+
174
+ Async do |task|
175
+ # since there has been an error, we must call the `@on_reject` method to see if it handles it appropriately (by not raising another error and giving a return value).
176
+ # if there is no `on_reject` available, we will just have to continue with the error propagation to the children.
177
+ if @on_reject.nil?
178
+ # no rejection handler exists, thus the children must bear the responsibility of handling the error
179
+ self.handle_imminent_reject(reason)
180
+ else
181
+ # an `@on_reject` handler exists, so lets see if it can reolve the current error with a value.
182
+ new_handled_value = nil
183
+ begin
184
+ new_handled_value = @on_reject.call(reason)
185
+ rescue => new_error_reason
186
+ # a new error occurred in the `@on_reject` handler, resulting in no resolvable value.
187
+ # we must now pass on the responsibility of handling it to the children.
188
+ self.handle_imminent_reject(new_error_reason)
189
+ else
190
+ # the `@on_reject` function handled the error appropriately and returned a value, so we may now resolve the children with that new value.
191
+ self.handle_imminent_resolve(new_handled_value)
192
+ end
193
+ end
194
+ end
195
+
196
+ nil
197
+ end
198
+
199
+ # @param on_resolve [(value) => next_value_or_promise, nil] the function to call when the promise is resolved with a value.
200
+ # @param on_reject [(reason) => next_value_or_promise, nil] the function to call when the promise is rejected (either manually or due to a raised error).
201
+ # @return [Promise] returns a new promise, so that multiple [then] and [catch] calls can be chained.
202
+ def then(on_resolve = nil, on_reject = nil)
203
+ chainable_promise = self.class.new(on_resolve, on_reject)
204
+
205
+ Async do |task|
206
+ case @status
207
+ when "pending"
208
+ # add the new promise as a child to the currently pending promise. once this promise resolves, the new child will be notified.
209
+ @children << chainable_promise
210
+ when "fulfilled"
211
+ # this promise has already been fulfilled, so the child must be notified of the resolved value immediately.
212
+ chainable_promise.resolve(@value)
213
+ when "rejected"
214
+ # this promise has already been rejected, so the child must be notified of the rejection reason immediately.
215
+ chainable_promise.reject(@reason)
216
+ end
217
+ end
218
+
219
+ chainable_promise
220
+ end
221
+
222
+ # A catch method is supposed to rescue any rejections that are made by the parent promise.
223
+ # it is syntactically equivalent to a `self.then(nil, on_reject)` call.
224
+ # @param on_reject [(reason) => next_value_or_promise, nil] the function to call when the promise is rejected (either manually or due to a raised error).
225
+ # @return [Promise] returns a new promise, so that multiple [then] and [catch] calls can be chained.
226
+ def catch(on_reject = nil)
227
+ self.then(nil, on_reject)
228
+ end
229
+
230
+ # Wait for the Promise to be resolved, or rejected.
231
+ # If the Promise was rejected, and you wait for it, then it will raise an error, which you will have to rescue externally.
232
+ # @returns [Any] The resolved value of type `T`.
233
+ # @raise [String, StandardError] The error/reason for the rejection of this Promise.
234
+ def wait
235
+ value = self.async_wait()
236
+ # if an error had existed for this promise, then we shall raise it now.
237
+ raise @reason unless @reason.nil?
238
+ # if `value` is a promise object, then we will have to await for it to resolve
239
+ return value.wait() if value.is_a?(Promise)
240
+ value
241
+ end
242
+
243
+ # Get the status of the this Promise.
244
+ # This should be used for debugging purposes only. your application logic MUST NOT depend on it, at all.
245
+ # @return ["pending", "fulfilled", "rejected"] Represents the current state of this Promise node.
246
+ def status
247
+ @status
248
+ end
249
+
250
+ # Provide a final resolved value for this promise node.
251
+ # @param value [Any] the final resolved value to commit to.
252
+ private def handle_imminent_resolve(value)
253
+ @status = "fulfilled"
254
+ @value = value
255
+ Async do |task|
256
+ @children.each { |child_promise| child_promise.resolve(value) }
257
+ end
258
+ self.async_resolve(value) # Ensure the underlying `Async::Variable` is resolved, so that the async library can stop waiting for it.
259
+ nil
260
+ end
261
+
262
+ # Provide a final rejection reason/error for this promise node.
263
+ # @param value [String, StandardError] the final error/reason for rejection to commit to.
264
+ private def handle_imminent_reject(reason)
265
+ @status = "rejected"
266
+ @reason = reason
267
+ # we are not going to raise the error here, irrespective of whether or not child promises are available.
268
+ # the error is intended to be ONLY risen when a rejected promise is awaited for via our overloaded `wait` method.
269
+ unless @children.empty?
270
+ # if there are child promises, we will pass the reason for rejection to each of them (and each must handle it, otherwise error exceptions will be raised when they are awaited for).
271
+ Async do |task|
272
+ @children.each { |child_promise| child_promise.reject(reason) }
273
+ end
274
+ end
275
+ self.async_resolve(nil) # Ensure the underlying `Async::Variable` is resolved with a `nil`, so that the async library can stop waiting for it.
276
+ nil
277
+ end
278
+ end
279
+ end
data/readme.md ADDED
@@ -0,0 +1,35 @@
1
+ # Async::Promise
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/async/promise`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/omar-azmi/async-promise-ruby.
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-promise
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Omar Azmi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.17'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.17'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.65'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.65'
55
+ - !ruby/object:Gem::Dependency
56
+ name: solargraph
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.50.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.50.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: sus
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.31.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.31.0
83
+ description: An Asynchronous Promise library for Ruby, built over the *async* gem,
84
+ providing Javascript ES6 style Promises.Also includes utilities like ES6-style *fetch*
85
+ that return a Promise.
86
+ email:
87
+ - 64020006+omar-azmi@users.noreply.github.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - changelog.md
93
+ - lib/async/promise.rb
94
+ - readme.md
95
+ homepage: https://github.com/omar-azmi/async-promise-ruby
96
+ licenses:
97
+ - CC-BY-NC-SA-4.0
98
+ metadata:
99
+ allowed_push_host: https://rubygems.org
100
+ homepage_uri: https://github.com/omar-azmi/async-promise-ruby
101
+ source_code_uri: https://github.com/omar-azmi/async-promise-ruby.git
102
+ changelog_uri: https://github.com/omar-azmi/async-promise-ruby/blob/main/changelog.md
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.1.1
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.5.16
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Asynchronous Javascript style Promises for Ruby.
122
+ test_files: []