prosody 0.4.0 → 0.5.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 +4 -4
- data/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +2 -2
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +660 -326
- data/Cargo.toml +2 -1
- data/README.md +290 -191
- data/examples/keyed_state.rb +15 -3
- data/examples/keyed_state_windowing.rb +9 -1
- data/ext/prosody/Cargo.toml +2 -1
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +194 -89
- data/ext/prosody/src/client/mod.rs +167 -74
- data/ext/prosody/src/client/request.rs +132 -0
- data/ext/prosody/src/client/support.rs +122 -0
- data/ext/prosody/src/handler/context.rs +24 -20
- data/ext/prosody/src/handler/message.rs +50 -0
- data/ext/prosody/src/handler/mod.rs +112 -84
- data/ext/prosody/src/handler/state/mod.rs +488 -0
- data/ext/prosody/src/handler/state/registration.rs +104 -0
- data/ext/prosody/src/handler/state/scan.rs +218 -0
- data/ext/prosody/src/lib.rs +15 -3
- data/ext/prosody/src/published.rs +273 -0
- data/ext/prosody/src/scheduler/mod.rs +2 -2
- data/ext/prosody/src/scheduler/processor.rs +2 -2
- data/ext/prosody/src/scheduler/result.rs +7 -4
- data/ext/prosody/src/util.rs +86 -5
- data/lib/prosody/configuration.rb +49 -15
- data/lib/prosody/handler.rb +63 -10
- data/lib/prosody/native_stubs.rb +197 -31
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +164 -41
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +1 -0
- data/sig/configuration.rbs +51 -15
- data/sig/handler.rbs +12 -4
- data/sig/prosody.rbs +43 -2
- data/sig/request.rbs +66 -0
- data/sig/state.rbs +165 -47
- data/steep_expectations.yml +10 -0
- data/typecheck/payload_types.rb +14 -3
- data/typecheck/payload_types.rbs +4 -2
- data/typecheck_negative/payload_types.rb +4 -0
- data/typecheck_negative/payload_types.rbs +1 -0
- metadata +12 -2
- data/ext/prosody/src/handler/state.rs +0 -1035
data/lib/prosody/native_stubs.rb
CHANGED
|
@@ -186,7 +186,7 @@ module Prosody
|
|
|
186
186
|
# +context.state(definition)+.
|
|
187
187
|
#
|
|
188
188
|
# @param name [String] the registered collection name
|
|
189
|
-
# @return [
|
|
189
|
+
# @return [NativeJsonValueState] the native handle
|
|
190
190
|
# @raise [PermanentStateError] if the name is unregistered or mismatched
|
|
191
191
|
# @private
|
|
192
192
|
def value_state(name)
|
|
@@ -199,7 +199,7 @@ module Prosody
|
|
|
199
199
|
# +context.state(definition)+.
|
|
200
200
|
#
|
|
201
201
|
# @param name [String] the registered collection name
|
|
202
|
-
# @return [
|
|
202
|
+
# @return [NativeJsonMapState] the native handle
|
|
203
203
|
# @raise [PermanentStateError] if the name is unregistered or mismatched
|
|
204
204
|
# @private
|
|
205
205
|
def map_state(name)
|
|
@@ -212,7 +212,7 @@ module Prosody
|
|
|
212
212
|
# +context.state(definition)+.
|
|
213
213
|
#
|
|
214
214
|
# @param name [String] the registered collection name
|
|
215
|
-
# @return [
|
|
215
|
+
# @return [NativeJsonDequeState] the native handle
|
|
216
216
|
# @raise [PermanentStateError] if the name is unregistered or mismatched
|
|
217
217
|
# @private
|
|
218
218
|
def deque_state(name)
|
|
@@ -226,7 +226,7 @@ module Prosody
|
|
|
226
226
|
# +context.state(definition)+.
|
|
227
227
|
#
|
|
228
228
|
# @param name [String] the registered collection name
|
|
229
|
-
# @return [
|
|
229
|
+
# @return [NativeMessageValueState] the native handle
|
|
230
230
|
# @raise [PermanentStateError] if the name is unregistered or mismatched
|
|
231
231
|
# @private
|
|
232
232
|
def message_value_state(name)
|
|
@@ -240,7 +240,7 @@ module Prosody
|
|
|
240
240
|
# +context.state(definition)+.
|
|
241
241
|
#
|
|
242
242
|
# @param name [String] the registered collection name
|
|
243
|
-
# @return [
|
|
243
|
+
# @return [NativeMessageMapState] the native handle
|
|
244
244
|
# @raise [PermanentStateError] if the name is unregistered or mismatched
|
|
245
245
|
# @private
|
|
246
246
|
def message_map_state(name)
|
|
@@ -253,7 +253,7 @@ module Prosody
|
|
|
253
253
|
# +context.state(definition)+.
|
|
254
254
|
#
|
|
255
255
|
# @param name [String] the registered collection name
|
|
256
|
-
# @return [
|
|
256
|
+
# @return [NativeMessageDequeState] the native handle
|
|
257
257
|
# @raise [PermanentStateError] if the name is unregistered or mismatched
|
|
258
258
|
# @private
|
|
259
259
|
def message_deque_state(name)
|
|
@@ -315,6 +315,24 @@ module Prosody
|
|
|
315
315
|
end
|
|
316
316
|
end
|
|
317
317
|
|
|
318
|
+
# An excise record with Kafka metadata and no payload.
|
|
319
|
+
class ExciseMessage
|
|
320
|
+
# @return [String] The topic name
|
|
321
|
+
def topic = raise NotImplementedError, "This method is implemented natively in Rust"
|
|
322
|
+
|
|
323
|
+
# @return [Integer] The partition number
|
|
324
|
+
def partition = raise NotImplementedError, "This method is implemented natively in Rust"
|
|
325
|
+
|
|
326
|
+
# @return [Integer] The message offset
|
|
327
|
+
def offset = raise NotImplementedError, "This method is implemented natively in Rust"
|
|
328
|
+
|
|
329
|
+
# @return [String] The message key
|
|
330
|
+
def key = raise NotImplementedError, "This method is implemented natively in Rust"
|
|
331
|
+
|
|
332
|
+
# @return [Time] The record timestamp
|
|
333
|
+
def timestamp = raise NotImplementedError, "This method is implemented natively in Rust"
|
|
334
|
+
end
|
|
335
|
+
|
|
318
336
|
# Represents a timer that was scheduled to fire at a specific time.
|
|
319
337
|
#
|
|
320
338
|
# Timer instances are created by the native code and passed to your
|
|
@@ -379,7 +397,8 @@ module Prosody
|
|
|
379
397
|
|
|
380
398
|
# Returns the current state of the consumer.
|
|
381
399
|
#
|
|
382
|
-
# The consumer can be in one of
|
|
400
|
+
# The consumer can be in one of four states:
|
|
401
|
+
# - `:shut_down` - The client is shut down
|
|
383
402
|
# - `:unconfigured` - The consumer has not been configured yet
|
|
384
403
|
# - `:configured` - The consumer is configured but not running
|
|
385
404
|
# - `:running` - The consumer is actively consuming messages
|
|
@@ -389,6 +408,10 @@ module Prosody
|
|
|
389
408
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
390
409
|
end
|
|
391
410
|
|
|
411
|
+
def native_request(_request)
|
|
412
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
413
|
+
end
|
|
414
|
+
|
|
392
415
|
# Returns the number of Kafka partitions currently assigned to this consumer.
|
|
393
416
|
#
|
|
394
417
|
# This method can be used to monitor the consumer's workload and ensure
|
|
@@ -426,11 +449,22 @@ module Prosody
|
|
|
426
449
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
427
450
|
end
|
|
428
451
|
|
|
452
|
+
# Sends an excise record to the specified Kafka topic.
|
|
453
|
+
#
|
|
454
|
+
# @param topic [String] The destination topic name
|
|
455
|
+
# @param key [String] The message key for partitioning
|
|
456
|
+
# @return [void]
|
|
457
|
+
# @raise [RuntimeError] If the excise record cannot be sent
|
|
458
|
+
def excise(topic, key)
|
|
459
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
460
|
+
end
|
|
461
|
+
|
|
429
462
|
# Subscribes to Kafka topics using the provided handler.
|
|
430
|
-
# The handler must implement
|
|
463
|
+
# The handler must implement `on_message`, `on_excise`, and `on_timer`.
|
|
431
464
|
#
|
|
432
|
-
# @param handler [EventHandler
|
|
465
|
+
# @param handler [EventHandler] A handler object that processes messages
|
|
433
466
|
# @return [void]
|
|
467
|
+
# @raise [ArgumentError] If a required handler method is missing
|
|
434
468
|
# @raise [RuntimeError] If subscription fails
|
|
435
469
|
#
|
|
436
470
|
# @example Subscribing with a handler
|
|
@@ -438,6 +472,13 @@ module Prosody
|
|
|
438
472
|
# def on_message(context, message)
|
|
439
473
|
# puts "Received message: #{message.payload}"
|
|
440
474
|
# end
|
|
475
|
+
#
|
|
476
|
+
# def on_excise(_context, message)
|
|
477
|
+
# puts "Excised key: #{message.key}"
|
|
478
|
+
# end
|
|
479
|
+
#
|
|
480
|
+
# def on_timer(_context, _timer)
|
|
481
|
+
# end
|
|
441
482
|
# end
|
|
442
483
|
#
|
|
443
484
|
# client.subscribe(MyHandler.new)
|
|
@@ -453,12 +494,21 @@ module Prosody
|
|
|
453
494
|
# @return [void]
|
|
454
495
|
# @raise [RuntimeError] If unsubscription fails
|
|
455
496
|
#
|
|
456
|
-
# @example Shutting down a consumer
|
|
457
|
-
# client.unsubscribe
|
|
458
497
|
def unsubscribe
|
|
459
498
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
460
499
|
end
|
|
461
500
|
|
|
501
|
+
# Shuts down the consumer and all client services.
|
|
502
|
+
#
|
|
503
|
+
# @return [void]
|
|
504
|
+
# @raise [RuntimeError] If shutdown fails
|
|
505
|
+
#
|
|
506
|
+
# @example Shutting down a client
|
|
507
|
+
# client.shutdown
|
|
508
|
+
def shutdown
|
|
509
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
510
|
+
end
|
|
511
|
+
|
|
462
512
|
# Returns the configured source system identifier.
|
|
463
513
|
#
|
|
464
514
|
# The source system is used to identify the originating service or
|
|
@@ -471,14 +521,29 @@ module Prosody
|
|
|
471
521
|
def source_system
|
|
472
522
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
473
523
|
end
|
|
524
|
+
|
|
525
|
+
# @private
|
|
526
|
+
def published_value(subsystem, name, read_cache, read_cache_disabled)
|
|
527
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# @private
|
|
531
|
+
def published_map(subsystem, name, read_cache, read_cache_disabled)
|
|
532
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# @private
|
|
536
|
+
def published_deque(subsystem, name, read_cache, read_cache_disabled)
|
|
537
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
538
|
+
end
|
|
474
539
|
end
|
|
475
540
|
|
|
476
541
|
# Native single-value keyed-state handle, vended by the context and wrapped by
|
|
477
542
|
# {Prosody::ValueState}. Every operation is fiber-yield async: it crosses the
|
|
478
543
|
# bridge and yields the fiber while the Rust core drives the operation.
|
|
479
544
|
#
|
|
480
|
-
# @see ext/prosody/src/handler/state.rs for implementation
|
|
481
|
-
|
|
545
|
+
# @see ext/prosody/src/handler/state/mod.rs for implementation
|
|
546
|
+
module NativeValueOperations
|
|
482
547
|
# @private
|
|
483
548
|
def initialize
|
|
484
549
|
raise NotImplementedError, "This class is implemented natively in Rust"
|
|
@@ -523,13 +588,21 @@ module Prosody
|
|
|
523
588
|
end
|
|
524
589
|
end
|
|
525
590
|
|
|
591
|
+
class NativeJsonValueState
|
|
592
|
+
include NativeValueOperations
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
class NativeMessageValueState
|
|
596
|
+
include NativeValueOperations
|
|
597
|
+
end
|
|
598
|
+
|
|
526
599
|
# Native String-keyed ordered-map keyed-state handle, vended by the context and
|
|
527
600
|
# wrapped by {Prosody::MapState}. Every operation is fiber-yield async, except
|
|
528
|
-
# +#scan+, which opens the cursor synchronously; each
|
|
601
|
+
# +#scan+, which opens the cursor synchronously; each native cursor pull
|
|
529
602
|
# yields the fiber.
|
|
530
603
|
#
|
|
531
|
-
# @see ext/prosody/src/handler/state.rs for implementation
|
|
532
|
-
|
|
604
|
+
# @see ext/prosody/src/handler/state/mod.rs for implementation
|
|
605
|
+
module NativeMapOperations
|
|
533
606
|
# @private
|
|
534
607
|
def initialize
|
|
535
608
|
raise NotImplementedError, "This class is implemented natively in Rust"
|
|
@@ -589,9 +662,9 @@ module Prosody
|
|
|
589
662
|
|
|
590
663
|
# Opens a native ordered scan over the live entries.
|
|
591
664
|
#
|
|
592
|
-
# @param direction [
|
|
593
|
-
# @return [
|
|
594
|
-
# @raise [TransientStateError] if direction is not
|
|
665
|
+
# @param direction [Symbol] +:forward+ or +:backward+
|
|
666
|
+
# @return [Object] the native cursor
|
|
667
|
+
# @raise [TransientStateError] if direction is not +:forward+ or +:backward+
|
|
595
668
|
def scan(direction)
|
|
596
669
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
597
670
|
end
|
|
@@ -600,9 +673,9 @@ module Prosody
|
|
|
600
673
|
# Skips value decode and the resolver (a message-backed map enumerates keys
|
|
601
674
|
# with zero Kafka fetches), though not no-I/O.
|
|
602
675
|
#
|
|
603
|
-
# @param direction [
|
|
604
|
-
# @return [
|
|
605
|
-
# @raise [TransientStateError] if direction is not
|
|
676
|
+
# @param direction [Symbol] +:forward+ or +:backward+
|
|
677
|
+
# @return [NativeMapKeyScan] the native key cursor
|
|
678
|
+
# @raise [TransientStateError] if direction is not +:forward+ or +:backward+
|
|
606
679
|
def keys(direction)
|
|
607
680
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
608
681
|
end
|
|
@@ -622,13 +695,21 @@ module Prosody
|
|
|
622
695
|
end
|
|
623
696
|
end
|
|
624
697
|
|
|
698
|
+
class NativeJsonMapState
|
|
699
|
+
include NativeMapOperations
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
class NativeMessageMapState
|
|
703
|
+
include NativeMapOperations
|
|
704
|
+
end
|
|
705
|
+
|
|
625
706
|
# Native deque keyed-state handle, vended by the context and wrapped by
|
|
626
707
|
# {Prosody::DequeState}. Every operation is fiber-yield async, except +#scan+,
|
|
627
|
-
# which opens the cursor synchronously; each
|
|
708
|
+
# which opens the cursor synchronously; each native cursor pull yields the
|
|
628
709
|
# fiber.
|
|
629
710
|
#
|
|
630
|
-
# @see ext/prosody/src/handler/state.rs for implementation
|
|
631
|
-
|
|
711
|
+
# @see ext/prosody/src/handler/state/mod.rs for implementation
|
|
712
|
+
module NativeDequeOperations
|
|
632
713
|
# @private
|
|
633
714
|
def initialize
|
|
634
715
|
raise NotImplementedError, "This class is implemented natively in Rust"
|
|
@@ -716,9 +797,9 @@ module Prosody
|
|
|
716
797
|
|
|
717
798
|
# Opens a native scan over the live elements.
|
|
718
799
|
#
|
|
719
|
-
# @param direction [
|
|
720
|
-
# @return [
|
|
721
|
-
# @raise [TransientStateError] if direction is not
|
|
800
|
+
# @param direction [Symbol] +:forward+ or +:backward+
|
|
801
|
+
# @return [Object] the native cursor
|
|
802
|
+
# @raise [TransientStateError] if direction is not +:forward+ or +:backward+
|
|
722
803
|
def scan(direction)
|
|
723
804
|
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
724
805
|
end
|
|
@@ -738,12 +819,20 @@ module Prosody
|
|
|
738
819
|
end
|
|
739
820
|
end
|
|
740
821
|
|
|
741
|
-
|
|
822
|
+
class NativeJsonDequeState
|
|
823
|
+
include NativeDequeOperations
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
class NativeMessageDequeState
|
|
827
|
+
include NativeDequeOperations
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
# Native cursor over a keyed-state collection, driven one chunk at a time
|
|
742
831
|
# by the {Prosody::MapState} / {Prosody::DequeState} traversal methods. Each
|
|
743
832
|
# pull crosses the bridge and yields the fiber; +close+ is idempotent.
|
|
744
833
|
#
|
|
745
|
-
# @see ext/prosody/src/handler/state.rs for implementation
|
|
746
|
-
|
|
834
|
+
# @see ext/prosody/src/handler/state/scan.rs for implementation
|
|
835
|
+
module NativeScanOperations
|
|
747
836
|
# @private
|
|
748
837
|
def initialize
|
|
749
838
|
raise NotImplementedError, "This class is implemented natively in Rust"
|
|
@@ -764,6 +853,83 @@ module Prosody
|
|
|
764
853
|
end
|
|
765
854
|
end
|
|
766
855
|
|
|
856
|
+
class NativeJsonDequeScan
|
|
857
|
+
include NativeScanOperations
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
class NativeJsonMapScan
|
|
861
|
+
include NativeScanOperations
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
class NativeMessageDequeScan
|
|
865
|
+
include NativeScanOperations
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
class NativeMessageMapScan
|
|
869
|
+
include NativeScanOperations
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
class NativeMapKeyScan
|
|
873
|
+
include NativeScanOperations
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
# @private
|
|
877
|
+
class NativePublishedValue
|
|
878
|
+
def get(key)
|
|
879
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# @private
|
|
884
|
+
class NativePublishedMap
|
|
885
|
+
def get(key, map_key)
|
|
886
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def get_many(key, map_keys)
|
|
890
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def contains_key(key, map_key)
|
|
894
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def scan(key, direction)
|
|
898
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def keys(key, direction)
|
|
902
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# @private
|
|
907
|
+
class NativePublishedDeque
|
|
908
|
+
def get(key, index)
|
|
909
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def length(key)
|
|
913
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def is_empty(key)
|
|
917
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def peek_front(key)
|
|
921
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def peek_back(key)
|
|
925
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def scan(key, direction)
|
|
929
|
+
raise NotImplementedError, "This method is implemented natively in Rust"
|
|
930
|
+
end
|
|
931
|
+
end
|
|
932
|
+
|
|
767
933
|
# Internal processor for executing tasks asynchronously.
|
|
768
934
|
# This class is used internally by the native code.
|
|
769
935
|
#
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosody
|
|
4
|
+
Success = Data.define(:value)
|
|
5
|
+
Failure = Data.define(:error)
|
|
6
|
+
HandlerError = Data.define(:message)
|
|
7
|
+
|
|
8
|
+
Timeout = Data.define do
|
|
9
|
+
def message = "no response arrived before the deadline"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
FormatMismatch = Data.define do
|
|
13
|
+
def message = "the responder answered in another format"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
MalformedResponse = Data.define do
|
|
17
|
+
def message = "the response did not decode"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Client
|
|
21
|
+
# Returns one outcome for each subsystem.
|
|
22
|
+
# @param timeout [Numeric] The response deadline in seconds.
|
|
23
|
+
# @raise [ArgumentError] if a subsystem name or timeout is invalid
|
|
24
|
+
# @raise [RuntimeError] if the request cannot start or the Kafka send fails
|
|
25
|
+
def request(topic:, key:, payload:, subsystems:, timeout:)
|
|
26
|
+
native_request(
|
|
27
|
+
topic: topic,
|
|
28
|
+
key: key,
|
|
29
|
+
payload: payload,
|
|
30
|
+
subsystems: subsystems,
|
|
31
|
+
timeout: timeout
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns one excise outcome for each subsystem.
|
|
36
|
+
def request_excise(topic:, key:, subsystems:, timeout:)
|
|
37
|
+
native_request_excise(
|
|
38
|
+
topic: topic,
|
|
39
|
+
key: key,
|
|
40
|
+
subsystems: subsystems,
|
|
41
|
+
timeout: timeout
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|