skyfall 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bc7ba74da06a265f8b1af42633b4c462f87fbf5764b38505628d9255bd97831
4
- data.tar.gz: 9bbb8e00563fdab6ee906ce82a3ddcde4936b8f0ee187da4d4f8032f5f344446
3
+ metadata.gz: ce06b8a7ef3783cd255621d6868a737735b782999a8cab76b70cf1f869e2cd1d
4
+ data.tar.gz: 9edea42903a17cb83ee717ea92c81e9a853b530a24f0bc803f70835b3bed1ba2
5
5
  SHA512:
6
- metadata.gz: 440c94077709828c54814ed8a4d520dbb513755412fa5baaee443d55f2493833e5c9196ffbcee256a118b8bdc6e41d92479fa041514ab8d9d16978aa4ec1d4c7
7
- data.tar.gz: b43da862f141b6ed476036eee85c8514a3b93a7a93bcaa16e089ee6149137c6b548639351445b2d3bddb3e2c458e8dc6e2c3eaa3f71d2efb9d92dad734f2fbf1
6
+ metadata.gz: 1b04bb91b734fe84d984af3c6a8c78959230d5ef865109882d86c5f0b2537f0a8b25678dcace7f5d8c26e560f871edba4de07f38273e2e7cf17088fd125a08e2
7
+ data.tar.gz: 5dffcf8a308789995d16293a9b8caba848374ab80c147a0ef87f440cb2c98887b4d81f3eb447d74c2bfc608ae448b54d9ada90556eb6e55cf59cc1df37e15390
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [0.2.0] - 2023-07-24
2
+
3
+ - switched the websocket library from `websocket-client-simple` to `faye-websocket`, which should make event parsing up to ~30× faster (!)
4
+ - added `auto_reconnect` property to `Stream` (on by default) - if true, it will try to reconnect with an exponential backoff when the websocket disconnects, until you call `Stream#disconnect`
5
+
6
+ Note:
7
+
8
+ - calling `sleep` is no longer needed after connecting - call `connect` on a new thread instead to get previously default behavior of running the event loop asynchronously
9
+ - the disconnect event no longer passes an error object in the argument
10
+ - there is currently no "heartbeat" feature as in 0.1.x that checks for a stuck connection - but it doesn't seem to be needed
11
+
12
+ ## [0.1.3] - 2023-07-04
13
+
14
+ - allow passing a previously saved cursor to websocket to replay any missed events
15
+ - the cursor is also kept in memory and automatically used when reconnecting
16
+ - added "connecting" callback with url as argument
17
+ - fixed connecting to websocket when endpoint is given as a string
18
+ - improved error handling during parsing
19
+
1
20
  ## [0.1.2] - 2023-06-15
2
21
 
3
22
  - added rkey property for Operation
data/README.md CHANGED
@@ -41,8 +41,6 @@ When you're ready, open the connection by calling `connect`:
41
41
  sky.connect
42
42
  ```
43
43
 
44
- The connection is started asynchronously on a separate thread. If you're running this code in a simple script (and not as a part of a server), call e.g. `sleep` without arguments or `loop { STDIN.read }` to prevent the script for exiting while the connection is open.
45
-
46
44
 
47
45
  ### Processing messages
48
46
 
data/example/firehose.rb CHANGED
@@ -21,7 +21,7 @@ end
21
21
 
22
22
  sky.on_connect { puts "Connected" }
23
23
  sky.on_disconnect { puts "Disconnected" }
24
+ sky.on_reconnect { puts "Reconnecting..." }
24
25
  sky.on_error { |e| puts "ERROR: #{e}" }
25
26
 
26
27
  sky.connect
27
- sleep
@@ -4,4 +4,15 @@ module Skyfall
4
4
 
5
5
  class UnsupportedError < StandardError
6
6
  end
7
+
8
+ class SubscriptionError < StandardError
9
+ attr_reader :error_type, :error_message
10
+
11
+ def initialize(error_type, error_message = nil)
12
+ @error_type = error_type
13
+ @error_message = error_message
14
+
15
+ super("Subscription error: #{error_type}" + (error_message ? " (#{error_message})" : ""))
16
+ end
17
+ end
7
18
  end
@@ -1,5 +1,8 @@
1
1
  require_relative 'websocket_message'
2
- require 'websocket-client-simple'
2
+
3
+ require 'eventmachine'
4
+ require 'faye/websocket'
5
+ require 'uri'
3
6
 
4
7
  module Skyfall
5
8
  class Stream
@@ -9,101 +12,84 @@ module Skyfall
9
12
  :subscribe_repos => SUBSCRIBE_REPOS
10
13
  }
11
14
 
12
- attr_accessor :heartbeat_timeout, :heartbeat_interval
15
+ MAX_RECONNECT_INTERVAL = 300
16
+
17
+ attr_accessor :heartbeat_timeout, :heartbeat_interval, :cursor, :auto_reconnect
13
18
 
14
- def initialize(server, endpoint)
19
+ def initialize(server, endpoint, cursor = nil)
15
20
  @endpoint = check_endpoint(endpoint)
16
21
  @server = check_hostname(server)
22
+ @cursor = cursor
17
23
  @handlers = {}
18
24
  @heartbeat_mutex = Mutex.new
19
25
  @heartbeat_interval = 5
20
26
  @heartbeat_timeout = 30
21
27
  @last_update = nil
28
+ @auto_reconnect = true
29
+ @connection_attempts = 0
22
30
  end
23
31
 
24
32
  def connect
25
- return if @websocket
26
-
27
- url = "wss://#{@server}/xrpc/#{@endpoint}"
28
- handlers = @handlers
29
- stream = self
33
+ return if @ws
30
34
 
31
- @websocket = WebSocket::Client::Simple.connect(url) do |ws|
32
- ws.on :message do |msg|
33
- stream.notify_heartbeat
34
- handlers[:raw_message]&.call(msg.data)
35
+ url = build_websocket_url
35
36
 
36
- if handlers[:message]
37
- atp_message = Skyfall::WebsocketMessage.new(msg.data)
38
- handlers[:message].call(atp_message)
39
- end
40
- end
37
+ @handlers[:connecting]&.call(url)
38
+ @engines_on = true
41
39
 
42
- ws.on :open do
43
- handlers[:connect]&.call
40
+ EM.run do
41
+ EventMachine.error_handler do |e|
42
+ @handlers[:error]&.call(e)
44
43
  end
45
44
 
46
- ws.on :close do |e|
47
- handlers[:disconnect]&.call(e)
48
- end
45
+ @ws = Faye::WebSocket::Client.new(url)
49
46
 
50
- ws.on :error do |e|
51
- handlers[:error]&.call(e)
47
+ @ws.on(:open) do |e|
48
+ @handlers[:connect]&.call
52
49
  end
53
- end
54
50
 
55
- if @heartbeat_interval && @heartbeat_timeout && @heartbeat_thread.nil?
56
- hb_interval = @heartbeat_interval
57
- hb_timeout = @heartbeat_timeout
51
+ @ws.on(:message) do |msg|
52
+ @connection_attempts = 0
58
53
 
59
- @last_update = Time.now
54
+ data = msg.data.pack('C*')
55
+ @handlers[:raw_message]&.call(data)
60
56
 
61
- @heartbeat_thread = Thread.new do
62
- loop do
63
- sleep(hb_interval)
64
- @heartbeat_mutex.synchronize do
65
- if Time.now - @last_update > hb_timeout
66
- force_restart
67
- end
68
- end
57
+ if @handlers[:message]
58
+ atp_message = Skyfall::WebsocketMessage.new(data)
59
+ @cursor = atp_message.seq
60
+ @handlers[:message].call(atp_message)
61
+ else
62
+ @cursor = nil
69
63
  end
70
64
  end
71
- end
72
- end
73
65
 
74
- def force_restart
75
- @websocket.close
76
- @websocket = nil
66
+ @ws.on(:error) do |e|
67
+ @handlers[:error]&.call(e)
68
+ end
77
69
 
78
- timeout = 5
70
+ @ws.on(:close) do |e|
71
+ @ws = nil
79
72
 
80
- loop do
81
- begin
82
- @handlers[:reconnect]&.call
83
- connect
84
- break
85
- rescue Exception => e
86
- @handlers[:error]&.call(e)
87
- sleep(timeout)
88
- timeout *= 2
73
+ if @auto_reconnect && @engines_on
74
+ EM.add_timer(reconnect_delay) do
75
+ @connection_attempts += 1
76
+ @handlers[:reconnect]&.call
77
+ connect
78
+ end
79
+ else
80
+ @engines_on = false
81
+ @handlers[:disconnect]&.call
82
+ EM.stop_event_loop unless @ws
83
+ end
89
84
  end
90
85
  end
91
-
92
- @last_update = Time.now
93
86
  end
94
87
 
95
88
  def disconnect
96
- return unless @websocket
97
-
98
- @heartbeat_thread&.kill
99
- @heartbeat_thread = nil
100
-
101
- @websocket.close
102
- @websocket = nil
103
- end
89
+ return unless EM.reactor_running?
104
90
 
105
- def notify_heartbeat
106
- @heartbeat_mutex.synchronize { @last_update = Time.now }
91
+ @engines_on = false
92
+ EM.stop_event_loop
107
93
  end
108
94
 
109
95
  alias close disconnect
@@ -116,6 +102,10 @@ module Skyfall
116
102
  @handlers[:raw_message] = block
117
103
  end
118
104
 
105
+ def on_connecting(&block)
106
+ @handlers[:connecting] = block
107
+ end
108
+
119
109
  def on_connect(&block)
120
110
  @handlers[:connect] = block
121
111
  end
@@ -135,6 +125,20 @@ module Skyfall
135
125
 
136
126
  private
137
127
 
128
+ def reconnect_delay
129
+ if @connection_attempts == 0
130
+ 0
131
+ else
132
+ [2 ** (@connection_attempts - 1), MAX_RECONNECT_INTERVAL].min
133
+ end
134
+ end
135
+
136
+ def build_websocket_url
137
+ url = "wss://#{@server}/xrpc/#{@endpoint}"
138
+ url += "?cursor=#{@cursor}" if @cursor
139
+ url
140
+ end
141
+
138
142
  def check_endpoint(endpoint)
139
143
  if endpoint.is_a?(String)
140
144
  raise ArgumentError("Invalid endpoint name: #{endpoint}") if endpoint.strip.empty? || !endpoint.include?('.')
@@ -144,6 +148,8 @@ module Skyfall
144
148
  else
145
149
  raise ArgumentError("Endpoint should be a string or a symbol")
146
150
  end
151
+
152
+ endpoint
147
153
  end
148
154
 
149
155
  def check_hostname(server)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skyfall
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -15,15 +15,7 @@ module Skyfall
15
15
  attr_reader :type, :repo, :time, :seq, :commit, :prev, :blocks, :operations
16
16
 
17
17
  def initialize(data)
18
- objects = CBOR.decode_sequence(data)
19
- raise DecodeError.new("Invalid number of objects: #{objects.length}") unless objects.length == 2
20
-
21
- @type_object, @data_object = objects
22
- raise DecodeError.new("Invalid object type: #{@type_object}") unless @type_object.is_a?(Hash)
23
- raise DecodeError.new("Invalid object type: #{@data_object}") unless @data_object.is_a?(Hash)
24
- raise DecodeError.new("Missing data: #{@type_object}") unless @type_object['op'] && @type_object['t']
25
- raise DecodeError.new("Invalid message type: #{@type_object['t']}") unless @type_object['t'].start_with?('#')
26
- raise UnsupportedError.new("Unexpected CBOR object: #{@type_object}") unless @type_object['op'] == 1
18
+ @type_object, @data_object = decode_cbor_objects(data)
27
19
 
28
20
  @type = @type_object['t'][1..-1].to_sym
29
21
  @operations = []
@@ -54,5 +46,32 @@ module Skyfall
54
46
  vars = keys.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
55
47
  "#<#{self.class}:0x#{object_id} #{vars}>"
56
48
  end
49
+
50
+ private
51
+
52
+ def decode_cbor_objects(data)
53
+ objects = CBOR.decode_sequence(data)
54
+
55
+ if objects.length < 2
56
+ raise DecodeError.new("Malformed message: #{objects.inspect}")
57
+ elsif objects.length > 2
58
+ raise DecodeError.new("Invalid number of objects: #{objects.length}")
59
+ end
60
+
61
+ type_object, data_object = objects
62
+
63
+ if data_object['error']
64
+ raise SubscriptionError.new(data_object['error'], data_object['message'])
65
+ end
66
+
67
+ raise DecodeError.new("Invalid object type: #{type_object}") unless type_object.is_a?(Hash)
68
+ raise UnsupportedError.new("Unexpected CBOR object: #{type_object}") unless type_object['op'] == 1
69
+ raise DecodeError.new("Missing data: #{type_object} #{objects.inspect}") unless type_object['op'] && type_object['t']
70
+ raise DecodeError.new("Invalid message type: #{type_object['t']}") unless type_object['t'].start_with?('#')
71
+
72
+ raise DecodeError.new("Invalid object type: #{data_object}") unless data_object.is_a?(Hash)
73
+
74
+ [type_object, data_object]
75
+ end
57
76
  end
58
77
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skyfall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-15 00:00:00.000000000 Z
11
+ date: 2023-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base32
@@ -51,25 +51,19 @@ dependencies:
51
51
  - !ruby/object:Gem::Version
52
52
  version: 0.5.9.6
53
53
  - !ruby/object:Gem::Dependency
54
- name: websocket-client-simple
54
+ name: faye-websocket
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '0.6'
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: 0.6.1
59
+ version: 0.11.2
63
60
  type: :runtime
64
61
  prerelease: false
65
62
  version_requirements: !ruby/object:Gem::Requirement
66
63
  requirements:
67
64
  - - "~>"
68
65
  - !ruby/object:Gem::Version
69
- version: '0.6'
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: 0.6.1
66
+ version: 0.11.2
73
67
  description: "\n Skyfall is a Ruby library for connecting to the \"firehose\" of
74
68
  the Bluesky social network, i.e. a websocket which\n streams all new posts and
75
69
  everything else happening on the Bluesky network in real time. The code connects