skyfall 0.1.2 → 0.2.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: 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