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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +0 -2
- data/example/firehose.rb +1 -1
- data/lib/skyfall/errors.rb +11 -0
- data/lib/skyfall/stream.rb +70 -64
- data/lib/skyfall/version.rb +1 -1
- data/lib/skyfall/websocket_message.rb +28 -9
- metadata +5 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce06b8a7ef3783cd255621d6868a737735b782999a8cab76b70cf1f869e2cd1d
|
4
|
+
data.tar.gz: 9edea42903a17cb83ee717ea92c81e9a853b530a24f0bc803f70835b3bed1ba2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/lib/skyfall/errors.rb
CHANGED
@@ -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
|
data/lib/skyfall/stream.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require_relative 'websocket_message'
|
2
|
-
|
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
|
-
|
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 @
|
26
|
-
|
27
|
-
url = "wss://#{@server}/xrpc/#{@endpoint}"
|
28
|
-
handlers = @handlers
|
29
|
-
stream = self
|
33
|
+
return if @ws
|
30
34
|
|
31
|
-
|
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
|
-
|
37
|
-
|
38
|
-
handlers[:message].call(atp_message)
|
39
|
-
end
|
40
|
-
end
|
37
|
+
@handlers[:connecting]&.call(url)
|
38
|
+
@engines_on = true
|
41
39
|
|
42
|
-
|
43
|
-
|
40
|
+
EM.run do
|
41
|
+
EventMachine.error_handler do |e|
|
42
|
+
@handlers[:error]&.call(e)
|
44
43
|
end
|
45
44
|
|
46
|
-
ws
|
47
|
-
handlers[:disconnect]&.call(e)
|
48
|
-
end
|
45
|
+
@ws = Faye::WebSocket::Client.new(url)
|
49
46
|
|
50
|
-
ws.on
|
51
|
-
handlers[:
|
47
|
+
@ws.on(:open) do |e|
|
48
|
+
@handlers[:connect]&.call
|
52
49
|
end
|
53
|
-
end
|
54
50
|
|
55
|
-
|
56
|
-
|
57
|
-
hb_timeout = @heartbeat_timeout
|
51
|
+
@ws.on(:message) do |msg|
|
52
|
+
@connection_attempts = 0
|
58
53
|
|
59
|
-
|
54
|
+
data = msg.data.pack('C*')
|
55
|
+
@handlers[:raw_message]&.call(data)
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
@
|
65
|
-
|
66
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
66
|
+
@ws.on(:error) do |e|
|
67
|
+
@handlers[:error]&.call(e)
|
68
|
+
end
|
77
69
|
|
78
|
-
|
70
|
+
@ws.on(:close) do |e|
|
71
|
+
@ws = nil
|
79
72
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
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
|
-
|
106
|
-
|
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)
|
data/lib/skyfall/version.rb
CHANGED
@@ -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
|
-
|
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.
|
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-
|
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
|
54
|
+
name: faye-websocket
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
56
56
|
requirements:
|
57
57
|
- - "~>"
|
58
58
|
- !ruby/object:Gem::Version
|
59
|
-
version:
|
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:
|
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
|