devcycle-ruby-server-sdk 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 960c105106387fcb10b360f0befbb77d2dda8c80dce5a51a5eed3c8c49528253
4
- data.tar.gz: 1c9274022cece19d953ceba7adfed10cd6418e4a3097ad3305f1a539ace32e23
3
+ metadata.gz: 66277b1d5bea79db7794168b2f9be005a3c37ef6ea0dc1d9b0cbf876c5fb1c17
4
+ data.tar.gz: 94381d579783bffc333ac56391ea530466c796a44ce700c25dc99267409412ed
5
5
  SHA512:
6
- metadata.gz: 67cb4a92cb5c9247bd10c838cfa0979d89d331c75c5626967af9b4602c55c30a2842f94f2c15471bb459686382be20432e43bf9951f1c27f516cbc5b2d8a504e
7
- data.tar.gz: 802ec0ab8f6b2a7623fe58c94cd007484832c06c5742a38831bebd0991ef2c3b57cc99563ffcee89a545742c0fe1080beea95be01aab88f27983d5d808b359f1
6
+ metadata.gz: 9b958f804a26259dadd8f33d46a0326d9c4ca5a8aad5c0a230ffdbb54d0adc7d82714677add429dc5161c855d31a0e1a19fdc0461db70a0945436285160996b2
7
+ data.tar.gz: 731696f8b1129e8151031d8ede467f646f0c910f747a36dfc9ab54b127db6097eddfcbb35afc2cf1d49da2d53a8c22e20e0d908f9a6371845a7f67e63a84b701
@@ -32,8 +32,8 @@ module DevCycle
32
32
  @api_client.config.enable_edge_db = @dvc_options.enable_edge_db
33
33
  @api_client.config.logger = @logger
34
34
  else
35
- @localbucketing = LocalBucketing.new(@sdkKey, dvc_options, wait_for_init)
36
- @event_queue = EventQueue.new(@sdkKey, dvc_options.event_queue_options, @localbucketing)
35
+ @local_bucketing = LocalBucketing.new(@sdkKey, dvc_options, wait_for_init)
36
+ @event_queue = EventQueue.new(@sdkKey, dvc_options.event_queue_options, @local_bucketing)
37
37
  end
38
38
  end
39
39
 
@@ -42,15 +42,15 @@ module DevCycle
42
42
  @logger.info("Cloud Bucketing does not require closing.")
43
43
  return
44
44
  end
45
- if @localbucketing != nil
46
- if !@localbucketing.initialized
45
+ if @local_bucketing != nil
46
+ if !@local_bucketing.initialized
47
47
  @logger.info("Awaiting client initialization before closing")
48
- while !@localbucketing.initialized
48
+ while !@local_bucketing.initialized
49
49
  sleep(0.5)
50
50
  end
51
51
  end
52
- @localbucketing.close
53
- @localbucketing = nil
52
+ @local_bucketing.close
53
+ @local_bucketing = nil
54
54
  @logger.info("Closed DevCycle Local Bucketing Engine.")
55
55
  end
56
56
 
@@ -65,7 +65,7 @@ module DevCycle
65
65
  end
66
66
 
67
67
  if local_bucketing_initialized?
68
- @localbucketing.set_client_custom_data(customdata)
68
+ @local_bucketing.set_client_custom_data(customdata)
69
69
  else
70
70
  @logger.warn("Local bucketing not initialized. Unable to set client custom data.")
71
71
  end
@@ -93,8 +93,8 @@ module DevCycle
93
93
  return data
94
94
  end
95
95
 
96
- if local_bucketing_initialized? && @localbucketing.has_config
97
- bucketed_config = @localbucketing.generate_bucketed_config(user_data)
96
+ if local_bucketing_initialized? && @local_bucketing.has_config
97
+ bucketed_config = @local_bucketing.generate_bucketed_config(user_data)
98
98
  bucketed_config.features
99
99
  else
100
100
  {}
@@ -176,60 +176,40 @@ module DevCycle
176
176
  return data
177
177
  end
178
178
 
179
- if local_bucketing_initialized? && @localbucketing.has_config
180
- bucketed_config = @localbucketing.generate_bucketed_config(user_data)
181
- variable_json = bucketed_config.variables[key]
179
+ value = default
180
+ type = determine_variable_type(default)
181
+ defaulted = true
182
+ if local_bucketing_initialized? && @local_bucketing.has_config
183
+ type_code = variable_type_code_from_type(type)
184
+ variable_json = variable_for_user(user_data, key, type_code)
182
185
  if variable_json == nil
183
186
  @logger.warn("No variable found for key #{key}, returning default value")
184
- variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
185
- @event_queue.queue_aggregate_event(variable_event, bucketed_config)
186
-
187
- return Variable.new({
188
- key: key,
189
- type: determine_variable_type(default),
190
- value: default,
191
- defaultValue: default,
192
- isDefaulted: true
193
- })
194
- end
195
- default_type = determine_variable_type(default)
196
- variable_type = variable_json['type']
197
- if default_type != variable_type
187
+ elsif type != variable_json['type']
198
188
  @logger.warn("Type mismatch for variable #{key}, returning default value")
199
- variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
200
- @event_queue.queue_aggregate_event(variable_event, bucketed_config)
201
-
202
- return Variable.new({
203
- key: key,
204
- type: default_type,
205
- value: default,
206
- defaultValue: default,
207
- isDefaulted: true
208
- })
189
+ else
190
+ value = variable_json['value']
191
+ defaulted = false
209
192
  end
210
- variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_evaluated], target: key })
211
- @event_queue.queue_aggregate_event(variable_event, bucketed_config)
212
-
213
- Variable.new({
214
- key: key,
215
- type: variable_type,
216
- value: variable_json['value'],
217
- defaultValue: default,
218
- isDefaulted: false
219
- })
220
193
  else
221
194
  @logger.warn("Local bucketing not initialized, returning default value for variable #{key}")
222
195
  variable_event = Event.new({ type: DevCycle::EventTypes[:agg_variable_defaulted], target: key })
196
+ bucketed_config = BucketedUserConfig.new({}, {}, {}, {}, {}, {}, [])
223
197
  @event_queue.queue_aggregate_event(variable_event, bucketed_config)
224
-
225
- Variable.new({
226
- key: key,
227
- type: determine_variable_type(default),
228
- value: default,
229
- defaultValue: default,
230
- isDefaulted: true
231
- })
232
198
  end
199
+
200
+ Variable.new({
201
+ key: key,
202
+ value: value,
203
+ type: type,
204
+ defaultValue: default,
205
+ isDefaulted: defaulted
206
+ })
207
+ end
208
+
209
+ def variable_for_user(user, key, variable_type_code)
210
+ json_str = @local_bucketing.variable_for_user(user, key, variable_type_code)
211
+ return nil if json_str.nil?
212
+ JSON.parse(json_str)
233
213
  end
234
214
 
235
215
  # Get variable by key for user data
@@ -319,8 +299,8 @@ module DevCycle
319
299
  return data
320
300
  end
321
301
 
322
- if local_bucketing_initialized? && @localbucketing.has_config
323
- bucketed_config = @localbucketing.generate_bucketed_config(user_data)
302
+ if local_bucketing_initialized? && @local_bucketing.has_config
303
+ bucketed_config = @local_bucketing.generate_bucketed_config(user_data)
324
304
  bucketed_config.variables
325
305
  else
326
306
  {}
@@ -486,7 +466,7 @@ module DevCycle
486
466
  end
487
467
 
488
468
  def local_bucketing_initialized?
489
- !@localbucketing.nil? && @localbucketing.initialized
469
+ !@local_bucketing.nil? && @local_bucketing.initialized
490
470
  end
491
471
 
492
472
  def determine_variable_type(variable_value)
@@ -502,5 +482,20 @@ module DevCycle
502
482
  raise ArgumentError, "Invalid type for variable: #{variable_value}"
503
483
  end
504
484
  end
485
+
486
+ def variable_type_code_from_type(type)
487
+ case type
488
+ when 'String'
489
+ @local_bucketing.variable_type_codes[:string]
490
+ when 'Boolean'
491
+ @local_bucketing.variable_type_codes[:boolean]
492
+ when 'Number'
493
+ @local_bucketing.variable_type_codes[:number]
494
+ when 'JSON'
495
+ @local_bucketing.variable_type_codes[:json]
496
+ else
497
+ raise ArgumentError.new("Invalid type for variable: #{type}")
498
+ end
499
+ end
505
500
  end
506
501
  end
@@ -13,6 +13,7 @@ module DevCycle
13
13
  extend T::Sig
14
14
 
15
15
  attr_reader :options
16
+ attr_reader :variable_type_codes
16
17
  attr_accessor :initialized
17
18
  attr_accessor :has_config
18
19
 
@@ -48,6 +49,8 @@ module DevCycle
48
49
  result
49
50
  }
50
51
 
52
+ @@stack_tracer_raise = lambda { |message| raise message }
53
+ # each method reassigns stack_tracer so the call stack is properly displayed
51
54
  @@stack_tracer = lambda {}
52
55
 
53
56
  @@linker.func_new("env", "abort", [:i32, :i32, :i32, :i32], []) do |_caller, messagePtr, filenamePtr, lineNum, colNum|
@@ -91,6 +94,13 @@ module DevCycle
91
94
  @sdkkey = sdkkey
92
95
  @options = options
93
96
  @logger = options.logger
97
+ @wasm_mutex = Mutex.new
98
+ @variable_type_codes = {
99
+ boolean: @@instance.export("VariableType.Boolean").to_global.get.to_i,
100
+ string: @@instance.export("VariableType.String").to_global.get.to_i,
101
+ number: @@instance.export("VariableType.Number").to_global.get.to_i,
102
+ json: @@instance.export("VariableType.JSON").to_global.get.to_i
103
+ }
94
104
  set_sdk_key_internal(sdkkey)
95
105
  platform_data = PlatformData.new('server', VERSION, RUBY_VERSION, nil, 'Ruby', Socket.gethostname)
96
106
  set_platform_data(platform_data)
@@ -104,123 +114,158 @@ module DevCycle
104
114
 
105
115
  sig { params(user: UserData).returns(BucketedUserConfig) }
106
116
  def generate_bucketed_config(user)
107
- user_addr = malloc_asc_string(user.to_json)
108
- @@stack_tracer = lambda { |message| raise message }
109
- config_addr = @@instance.invoke("generateBucketedConfigForUser", @sdkKeyAddr, user_addr)
110
- bucketed_config_json = read_asc_string(config_addr)
111
- bucketed_config_hash = Oj.load(bucketed_config_json)
112
-
113
- BucketedUserConfig.new(bucketed_config_hash['project'],
114
- bucketed_config_hash['environment'],
115
- bucketed_config_hash['features'],
116
- bucketed_config_hash['featureVariationMap'],
117
- bucketed_config_hash['variableVariationMap'],
118
- bucketed_config_hash['variables'],
119
- bucketed_config_hash['knownVariableKeys'])
117
+ @wasm_mutex.synchronize do
118
+ user_addr = malloc_asc_string(user.to_json)
119
+ @@stack_tracer = @@stack_tracer_raise
120
+ config_addr = @@instance.invoke("generateBucketedConfigForUser", @sdkKeyAddr, user_addr)
121
+ bucketed_config_json = read_asc_string(config_addr)
122
+ bucketed_config_hash = Oj.load(bucketed_config_json)
123
+
124
+ BucketedUserConfig.new(bucketed_config_hash['project'],
125
+ bucketed_config_hash['environment'],
126
+ bucketed_config_hash['features'],
127
+ bucketed_config_hash['featureVariationMap'],
128
+ bucketed_config_hash['variableVariationMap'],
129
+ bucketed_config_hash['variables'],
130
+ bucketed_config_hash['knownVariableKeys'])
131
+ end
132
+ end
133
+
134
+ sig { params(user: UserData, key: String, variable_type: Integer).returns(T.nilable(String)) }
135
+ def variable_for_user(user, key, variable_type)
136
+ @wasm_mutex.synchronize do
137
+ user_addr = malloc_asc_string(user.to_json)
138
+ key_addr = malloc_asc_string(key)
139
+ @@stack_tracer = @@stack_tracer_raise
140
+ var_addr = @@instance.invoke("variableForUser", @sdkKeyAddr, user_addr, key_addr, variable_type, 1)
141
+ read_asc_string(var_addr)
142
+ end
120
143
  end
121
144
 
122
145
  sig { returns(T::Array[EventsPayload]) }
123
146
  def flush_event_queue
124
- @@stack_tracer = lambda { |message| raise message }
125
- payload_addr = @@instance.invoke("flushEventQueue", @sdkKeyAddr)
126
- raw_json = read_asc_string(payload_addr)
127
- raw_payloads = Oj.load(raw_json)
128
-
129
- if raw_payloads == nil
130
- return []
147
+ @wasm_mutex.synchronize do
148
+ @@stack_tracer = @@stack_tracer_raise
149
+ payload_addr = @@instance.invoke("flushEventQueue", @sdkKeyAddr)
150
+ raw_json = read_asc_string(payload_addr)
151
+ raw_payloads = Oj.load(raw_json)
152
+
153
+ if raw_payloads == nil
154
+ return []
155
+ end
156
+ raw_payloads.map { |raw_payload| EventsPayload.new(raw_payload["records"], raw_payload["payloadId"], raw_payload["eventCount"]) }
131
157
  end
132
- raw_payloads.map { |raw_payload| EventsPayload.new(raw_payload["records"], raw_payload["payloadId"], raw_payload["eventCount"]) }
133
158
  end
134
159
 
135
160
  sig { returns(Integer) }
136
161
  def check_event_queue_size
137
- @@stack_tracer = lambda { |message| raise message }
138
- @@instance.invoke("eventQueueSize", @sdkKeyAddr)
162
+ @wasm_mutex.synchronize do
163
+ @@stack_tracer = @@stack_tracer_raise
164
+ @@instance.invoke("eventQueueSize", @sdkKeyAddr)
165
+ end
139
166
  end
140
167
 
141
168
  sig { params(payload_id: String).returns(NilClass) }
142
169
  def on_payload_success(payload_id)
143
- payload_addr = malloc_asc_string(payload_id)
144
- @@stack_tracer = lambda { |message| raise message }
145
- @@instance.invoke("onPayloadSuccess", @sdkKeyAddr, payload_addr)
170
+ @wasm_mutex.synchronize do
171
+ payload_addr = malloc_asc_string(payload_id)
172
+ @@stack_tracer = @@stack_tracer_raise
173
+ @@instance.invoke("onPayloadSuccess", @sdkKeyAddr, payload_addr)
174
+ end
175
+ end
176
+
177
+ sig { params(payload_id: String, retryable: Object).returns(NilClass) }
178
+ def on_payload_failure(payload_id, retryable)
179
+ @wasm_mutex.synchronize do
180
+ payload_addr = malloc_asc_string(payload_id)
181
+ @@stack_tracer = @@stack_tracer_raise
182
+ @@instance.invoke("onPayloadFailure", @sdkKeyAddr, payload_addr, retryable ? 1 : 0)
183
+ end
146
184
  end
147
185
 
148
186
  sig { params(user: UserData, event: Event).returns(NilClass) }
149
187
  def queue_event(user, event)
150
- begin
151
- user_addr = malloc_asc_string(Oj.dump(user))
152
- asc_pin(user_addr)
153
- event_addr = malloc_asc_string(Oj.dump(event))
154
- @@stack_tracer = lambda { |message| raise message }
155
- @@instance.invoke("queueEvent", @sdkKeyAddr, user_addr, event_addr)
156
- ensure
157
- asc_unpin(user_addr)
188
+ @wasm_mutex.synchronize do
189
+ begin
190
+ user_addr = malloc_asc_string(Oj.dump(user))
191
+ asc_pin(user_addr)
192
+ event_addr = malloc_asc_string(Oj.dump(event))
193
+ @@stack_tracer = @@stack_tracer_raise
194
+ @@instance.invoke("queueEvent", @sdkKeyAddr, user_addr, event_addr)
195
+ ensure
196
+ asc_unpin(user_addr)
197
+ end
158
198
  end
159
199
  end
160
200
 
161
201
  sig { params(event: Event, bucketeduser: T.nilable(BucketedUserConfig)).returns(NilClass) }
162
202
  def queue_aggregate_event(event, bucketeduser)
163
- begin
164
- variable_variation_map =
165
- if !bucketeduser.nil?
166
- bucketeduser.variable_variation_map
167
- else
168
- {}
169
- end
170
- varmap_addr = malloc_asc_string(Oj.dump(variable_variation_map))
171
- asc_pin(varmap_addr)
172
- event_addr = malloc_asc_string(Oj.dump(event))
173
- @@stack_tracer = lambda { |message| raise message }
174
- @@instance.invoke("queueAggregateEvent", @sdkKeyAddr, event_addr, varmap_addr)
175
- ensure
176
- asc_unpin(varmap_addr)
203
+ @wasm_mutex.synchronize do
204
+ begin
205
+ variable_variation_map =
206
+ if !bucketeduser.nil?
207
+ bucketeduser.variable_variation_map
208
+ else
209
+ {}
210
+ end
211
+ varmap_addr = malloc_asc_string(Oj.dump(variable_variation_map))
212
+ asc_pin(varmap_addr)
213
+ event_addr = malloc_asc_string(Oj.dump(event))
214
+ @@stack_tracer = @@stack_tracer_raise
215
+ @@instance.invoke("queueAggregateEvent", @sdkKeyAddr, event_addr, varmap_addr)
216
+ ensure
217
+ asc_unpin(varmap_addr)
218
+ end
177
219
  end
178
220
  end
179
221
 
180
- sig { params(payload_id: String, retryable: Object).returns(NilClass) }
181
- def on_payload_failure(payload_id, retryable)
182
- payload_addr = malloc_asc_string(payload_id)
183
- @@stack_tracer = lambda { |message| raise message }
184
- @@instance.invoke("onPayloadFailure", @sdkKeyAddr, payload_addr, retryable ? 1 : 0)
185
- end
186
-
187
222
  sig { params(config: String).returns(NilClass) }
188
223
  def store_config(config)
189
- config_addr = malloc_asc_string(config)
190
- @@stack_tracer = lambda { |message| raise message }
191
- @@instance.invoke("setConfigData", @sdkKeyAddr, config_addr)
224
+ @wasm_mutex.synchronize do
225
+ config_addr = malloc_asc_string(config)
226
+ @@stack_tracer = @@stack_tracer_raise
227
+ @@instance.invoke("setConfigData", @sdkKeyAddr, config_addr)
228
+ end
192
229
  end
193
230
 
194
231
  sig { params(options: EventQueueOptions).returns(NilClass) }
195
232
  def init_event_queue(options)
196
- options_json = Oj.dump(options)
197
- options_addr = malloc_asc_string(options_json)
198
- @@stack_tracer = lambda { |message| raise message }
199
- @@instance.invoke("initEventQueue", @sdkKeyAddr, options_addr)
233
+ @wasm_mutex.synchronize do
234
+ options_json = Oj.dump(options)
235
+ options_addr = malloc_asc_string(options_json)
236
+ @@stack_tracer = @@stack_tracer_raise
237
+ @@instance.invoke("initEventQueue", @sdkKeyAddr, options_addr)
238
+ end
200
239
  end
201
240
 
202
241
  sig { params(customdata: Hash).returns(NilClass) }
203
242
  def set_client_custom_data(customdata)
204
- customdata_json = Oj.dump(customdata)
205
- customdata_addr = malloc_asc_string(customdata_json)
206
- @@stack_tracer = lambda { |message| raise message }
207
- @@instance.invoke("setClientCustomData", customdata_addr)
243
+ @wasm_mutex.synchronize do
244
+ customdata_json = Oj.dump(customdata)
245
+ customdata_addr = malloc_asc_string(customdata_json)
246
+ @@stack_tracer = @@stack_tracer_raise
247
+ @@instance.invoke("setClientCustomData", @sdkKeyAddr, customdata_addr)
248
+ end
208
249
  end
209
250
 
210
251
  private
211
252
 
212
253
  sig { params(platformdata: PlatformData).returns(NilClass) }
213
254
  def set_platform_data(platformdata)
214
- platformdata_json = Oj.dump(platformdata)
215
- platformdata_addr = malloc_asc_string(platformdata_json)
216
- @@stack_tracer = lambda { |message| raise message }
217
- @@instance.invoke("setPlatformData", platformdata_addr)
255
+ @wasm_mutex.synchronize do
256
+ platformdata_json = Oj.dump(platformdata)
257
+ platformdata_addr = malloc_asc_string(platformdata_json)
258
+ @@stack_tracer = @@stack_tracer_raise
259
+ @@instance.invoke("setPlatformData", platformdata_addr)
260
+ end
218
261
  end
219
262
 
220
263
  def set_sdk_key_internal(sdkKey)
221
- addr = malloc_asc_string(sdkKey)
222
- @sdkKeyAddr = addr
223
- asc_pin(addr)
264
+ @wasm_mutex.synchronize do
265
+ addr = malloc_asc_string(sdkKey)
266
+ @sdkKeyAddr = addr
267
+ asc_pin(addr)
268
+ end
224
269
  end
225
270
 
226
271
  def asc_pin(addr)
@@ -236,7 +281,7 @@ module DevCycle
236
281
  sig { params(string: String).returns(Integer) }
237
282
  def malloc_asc_string(string)
238
283
  wasm_object_id = 1
239
- @@stack_tracer = lambda { |message| raise message }
284
+ @@stack_tracer = @@stack_tracer_raise
240
285
  wasm_new = @@instance.export("__new").to_func
241
286
  utf8_bytes = string.bytes
242
287
  byte_len = utf8_bytes.length
@@ -244,8 +289,8 @@ module DevCycle
244
289
  start_addr = wasm_new.call(byte_len * 2, wasm_object_id)
245
290
  i = 0
246
291
  while i < byte_len
247
- @@stack_tracer = lambda { |message| raise message }
248
- @@memory.write(start_addr + (i * 2), [utf8_bytes[i]].pack('U'))
292
+ @@stack_tracer = @@stack_tracer_raise
293
+ @@memory.write(start_addr + (i * 2), [utf8_bytes[i]].pack('c'))
249
294
  i += 1
250
295
  end
251
296
  start_addr
@@ -253,23 +298,23 @@ module DevCycle
253
298
 
254
299
  # @param [Integer] address start address of string.
255
300
  # @return [String] resulting string
256
- sig { params(address: Integer).returns(String) }
301
+ sig { params(address: Integer).returns(T.nilable(String)) }
257
302
  def read_asc_string(address)
258
- @@stack_tracer = lambda { |message| raise message }
303
+ if address == 0
304
+ @logger.debug("null address passed to read_asc_string")
305
+ return nil
306
+ end
307
+
308
+ @@stack_tracer = @@stack_tracer_raise
259
309
  raw_bytes = @@memory.read(address - 4, 4).bytes.reverse
260
310
  len = 0
261
311
  raw_bytes.each { |j|
262
312
  len = (len << 8) + (j & 0xFF)
263
313
  }
264
- result = ""
265
- i = 0
266
- while i < len
267
- @@stack_tracer = lambda { |message| raise message }
268
- result += @@memory.read(address + i, 1)
269
- i += 2
270
- end
271
- result
314
+
315
+ @@stack_tracer = @@stack_tracer_raise
316
+ result = @@memory.read(address, len).bytes
317
+ result.select.with_index { |_, i| i.even? }.pack('c*')
272
318
  end
273
319
  end
274
320
  end
275
-
@@ -11,5 +11,5 @@ OpenAPI Generator version: 5.3.0
11
11
  =end
12
12
 
13
13
  module DevCycle
14
- VERSION = '2.0.3'
14
+ VERSION = '2.1.0'
15
15
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devcycle-ruby-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DevCycleHQ
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-08 00:00:00.000000000 Z
11
+ date: 2023-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus