wsocket-io 0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +105 -0
- data/lib/wsocket_io.rb +405 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a49398de6e4dfd18df620b3e3fb1d367906fd9a13e4977cddf16e641b8f12b3b
|
|
4
|
+
data.tar.gz: a750836ba920e6a68b76bd9855ceaa6206158ae24f99fea80b3ae5d4ca07e0ee
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 57566fcd09e83871eabb16ef9b5d4cd11f4440a9a2e872968c86d48f082bb1ef575ad4a3a567a2f3838ac4d45dea0e5f8f804af7cdf1fc6b3ad53bd06225dfdd
|
|
7
|
+
data.tar.gz: 1a98eb61c17b8c86c341dc4d4dde05945bc4761e03bbd8b269636c7d5b0a9b8de855f6bc04cba8aeb50ad8b7408d9fae845549336254a177417c1e8db979e842
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wSocket
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# wSocket SDK for Ruby
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for wSocket — realtime pub/sub, presence, history, and push notifications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'wsocket-io'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install wsocket-io
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'wsocket_io'
|
|
29
|
+
|
|
30
|
+
client = WSocketIO::Client.new('wss://node00.wsocket.online', 'your-api-key')
|
|
31
|
+
|
|
32
|
+
client.on_connect { puts 'Connected!' }
|
|
33
|
+
|
|
34
|
+
client.connect
|
|
35
|
+
|
|
36
|
+
channel = client.pubsub.channel('chat')
|
|
37
|
+
|
|
38
|
+
channel.subscribe do |data, meta|
|
|
39
|
+
puts "Received: #{data}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
channel.publish({ text: 'Hello from Ruby!' })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Presence
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
channel = client.pubsub.channel('room')
|
|
49
|
+
|
|
50
|
+
channel.presence.enter(data: { name: 'Alice' })
|
|
51
|
+
|
|
52
|
+
channel.presence.on_enter do |member|
|
|
53
|
+
puts "#{member.client_id} entered"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
channel.presence.on_leave do |member|
|
|
57
|
+
puts "#{member.client_id} left"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
channel.presence.get
|
|
61
|
+
channel.presence.on_members do |members|
|
|
62
|
+
puts "Online: #{members.length}"
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## History
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
channel.history(limit: 50)
|
|
70
|
+
channel.on_history do |result|
|
|
71
|
+
result.messages.each do |msg|
|
|
72
|
+
puts "#{msg.publisher_id}: #{msg.data}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Push Notifications
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
push = WSocketIO::PushClient.new(
|
|
81
|
+
base_url: 'https://node00.wsocket.online',
|
|
82
|
+
token: 'secret',
|
|
83
|
+
app_id: 'app1'
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Register FCM device
|
|
87
|
+
push.register_fcm(device_token: fcm_token, member_id: 'user-123')
|
|
88
|
+
|
|
89
|
+
# Send to a member
|
|
90
|
+
push.send_to_member('user-123', payload: {
|
|
91
|
+
title: 'New message',
|
|
92
|
+
body: 'You have a new message'
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
# Broadcast
|
|
96
|
+
push.broadcast(payload: { title: 'Announcement', body: 'Server update' })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Requirements
|
|
100
|
+
|
|
101
|
+
- Ruby 3.0+
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
data/lib/wsocket_io.rb
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# wSocket Ruby SDK — Realtime Pub/Sub client with Presence, History, and Push.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# client = WSocketIO::Client.new('ws://localhost:9001', 'your-api-key')
|
|
7
|
+
# client.connect
|
|
8
|
+
# ch = client.pubsub.channel('chat')
|
|
9
|
+
# ch.subscribe { |data, meta| puts data }
|
|
10
|
+
# ch.publish({ text: 'hello' })
|
|
11
|
+
|
|
12
|
+
require 'json'
|
|
13
|
+
require 'websocket-client-simple'
|
|
14
|
+
require 'net/http'
|
|
15
|
+
require 'uri'
|
|
16
|
+
require 'securerandom'
|
|
17
|
+
require 'base64'
|
|
18
|
+
|
|
19
|
+
module WSocketIO
|
|
20
|
+
# ─── Types ──────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
MessageMeta = Struct.new(:id, :channel, :timestamp, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
PresenceMember = Struct.new(:client_id, :data, :joined_at, keyword_init: true) do
|
|
25
|
+
def initialize(client_id: '', data: nil, joined_at: 0)
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
HistoryMessage = Struct.new(:id, :channel, :data, :publisher_id, :timestamp, :sequence, keyword_init: true) do
|
|
31
|
+
def initialize(id: '', channel: '', data: nil, publisher_id: '', timestamp: 0, sequence: 0)
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
HistoryResult = Struct.new(:channel, :messages, :has_more, keyword_init: true) do
|
|
37
|
+
def initialize(channel: '', messages: [], has_more: false)
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Options = Struct.new(:auto_reconnect, :max_reconnect_attempts, :reconnect_delay, :token, :recover, keyword_init: true) do
|
|
43
|
+
def initialize(auto_reconnect: true, max_reconnect_attempts: 10, reconnect_delay: 1.0, token: nil, recover: true)
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ─── Presence ───────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
class Presence
|
|
51
|
+
def initialize(channel_name, send_fn)
|
|
52
|
+
@channel_name = channel_name
|
|
53
|
+
@send_fn = send_fn
|
|
54
|
+
@enter_cbs = []
|
|
55
|
+
@leave_cbs = []
|
|
56
|
+
@update_cbs = []
|
|
57
|
+
@members_cbs = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def enter(data: nil)
|
|
61
|
+
@send_fn.call({ action: 'presence.enter', channel: @channel_name, data: data })
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def leave
|
|
66
|
+
@send_fn.call({ action: 'presence.leave', channel: @channel_name })
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update(data)
|
|
71
|
+
@send_fn.call({ action: 'presence.update', channel: @channel_name, data: data })
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get
|
|
76
|
+
@send_fn.call({ action: 'presence.get', channel: @channel_name })
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_enter(&block) = (@enter_cbs << block; self)
|
|
81
|
+
def on_leave(&block) = (@leave_cbs << block; self)
|
|
82
|
+
def on_update(&block) = (@update_cbs << block; self)
|
|
83
|
+
def on_members(&block) = (@members_cbs << block; self)
|
|
84
|
+
|
|
85
|
+
def handle_event(action, data)
|
|
86
|
+
case action
|
|
87
|
+
when 'presence.enter'
|
|
88
|
+
m = parse_member(data)
|
|
89
|
+
@enter_cbs.each { |cb| cb.call(m) }
|
|
90
|
+
when 'presence.leave'
|
|
91
|
+
m = parse_member(data)
|
|
92
|
+
@leave_cbs.each { |cb| cb.call(m) }
|
|
93
|
+
when 'presence.update'
|
|
94
|
+
m = parse_member(data)
|
|
95
|
+
@update_cbs.each { |cb| cb.call(m) }
|
|
96
|
+
when 'presence.members'
|
|
97
|
+
members = (data['members'] || []).map { |d| parse_member(d) }
|
|
98
|
+
@members_cbs.each { |cb| cb.call(members) }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def parse_member(d)
|
|
105
|
+
PresenceMember.new(
|
|
106
|
+
client_id: d['clientId'] || '',
|
|
107
|
+
data: d['data'],
|
|
108
|
+
joined_at: d['joinedAt'] || 0
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ─── Channel ────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
class Channel
|
|
116
|
+
attr_reader :name, :presence
|
|
117
|
+
|
|
118
|
+
def initialize(name, send_fn)
|
|
119
|
+
@name = name
|
|
120
|
+
@send_fn = send_fn
|
|
121
|
+
@message_cbs = []
|
|
122
|
+
@history_cbs = []
|
|
123
|
+
@presence = Presence.new(name, send_fn)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def subscribe(&callback)
|
|
127
|
+
@message_cbs << callback if callback
|
|
128
|
+
@send_fn.call({ action: 'subscribe', channel: @name })
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def unsubscribe
|
|
133
|
+
@send_fn.call({ action: 'unsubscribe', channel: @name })
|
|
134
|
+
@message_cbs.clear
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def publish(data, persist: nil)
|
|
139
|
+
msg = { action: 'publish', channel: @name, data: data, id: SecureRandom.uuid }
|
|
140
|
+
msg[:persist] = persist unless persist.nil?
|
|
141
|
+
@send_fn.call(msg)
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def history(limit: nil, before: nil, after: nil, direction: nil)
|
|
146
|
+
opts = { action: 'history', channel: @name }
|
|
147
|
+
opts[:limit] = limit if limit
|
|
148
|
+
opts[:before] = before if before
|
|
149
|
+
opts[:after] = after if after
|
|
150
|
+
opts[:direction] = direction if direction
|
|
151
|
+
@send_fn.call(opts)
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def on_history(&block)
|
|
156
|
+
@history_cbs << block
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def handle_message(data, meta)
|
|
161
|
+
@message_cbs.each { |cb| cb.call(data, meta) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_history(result)
|
|
165
|
+
@history_cbs.each { |cb| cb.call(result) }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# ─── PubSub Namespace ──────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
class PubSubNamespace
|
|
172
|
+
def initialize(client)
|
|
173
|
+
@client = client
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def channel(name)
|
|
177
|
+
@client.channel(name)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ─── Push Client ────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
class PushClient
|
|
184
|
+
def initialize(base_url:, token:, app_id:)
|
|
185
|
+
@base_url = base_url
|
|
186
|
+
@token = token
|
|
187
|
+
@app_id = app_id
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def register_fcm(device_token:, member_id:)
|
|
191
|
+
post('register', { memberId: member_id, platform: 'fcm', subscription: { deviceToken: device_token } })
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def register_apns(device_token:, member_id:)
|
|
195
|
+
post('register', { memberId: member_id, platform: 'apns', subscription: { deviceToken: device_token } })
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def send_to_member(member_id, payload:)
|
|
199
|
+
post('send', { memberId: member_id, payload: payload })
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def broadcast(payload:)
|
|
203
|
+
post('broadcast', { payload: payload })
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def unregister(member_id, platform: nil)
|
|
207
|
+
body = { memberId: member_id }
|
|
208
|
+
body[:platform] = platform if platform
|
|
209
|
+
uri = URI("#{@base_url}/api/push/unregister")
|
|
210
|
+
req = Net::HTTP::Delete.new(uri, headers)
|
|
211
|
+
req.body = body.to_json
|
|
212
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(req) }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def headers
|
|
218
|
+
{ 'Authorization' => "Bearer #{@token}", 'X-App-Id' => @app_id, 'Content-Type' => 'application/json' }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def post(path, body)
|
|
222
|
+
uri = URI("#{@base_url}/api/push/#{path}")
|
|
223
|
+
req = Net::HTTP::Post.new(uri, headers)
|
|
224
|
+
req.body = body.to_json
|
|
225
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(req) }
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ─── Client ─────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
class Client
|
|
232
|
+
attr_reader :pubsub
|
|
233
|
+
|
|
234
|
+
def initialize(url, api_key, options = Options.new)
|
|
235
|
+
@url = url
|
|
236
|
+
@api_key = api_key
|
|
237
|
+
@options = options
|
|
238
|
+
@channels = {}
|
|
239
|
+
@subscribed_channels = {}
|
|
240
|
+
@last_message_ts = 0
|
|
241
|
+
@reconnect_attempts = 0
|
|
242
|
+
@connected = false
|
|
243
|
+
@ws = nil
|
|
244
|
+
|
|
245
|
+
@on_connect_cbs = []
|
|
246
|
+
@on_disconnect_cbs = []
|
|
247
|
+
@on_error_cbs = []
|
|
248
|
+
|
|
249
|
+
@pubsub = PubSubNamespace.new(self)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def on_connect(&block) = (@on_connect_cbs << block; self)
|
|
253
|
+
def on_disconnect(&block) = (@on_disconnect_cbs << block; self)
|
|
254
|
+
def on_error(&block) = (@on_error_cbs << block; self)
|
|
255
|
+
def connected? = @connected
|
|
256
|
+
|
|
257
|
+
def connect
|
|
258
|
+
ws_url = @url.dup
|
|
259
|
+
ws_url += @url.include?('?') ? '&' : '?'
|
|
260
|
+
ws_url += "key=#{@api_key}"
|
|
261
|
+
ws_url += "&token=#{@options.token}" if @options.token
|
|
262
|
+
|
|
263
|
+
client = self
|
|
264
|
+
@ws = WebSocket::Client::Simple.connect(ws_url) do |ws|
|
|
265
|
+
ws.on :open do
|
|
266
|
+
client.send(:handle_open)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
ws.on :message do |msg|
|
|
270
|
+
client.send(:handle_raw_message, msg.data)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
ws.on :close do |e|
|
|
274
|
+
client.send(:handle_close, e)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
ws.on :error do |e|
|
|
278
|
+
client.send(:handle_error, e)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
self
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def disconnect
|
|
286
|
+
@connected = false
|
|
287
|
+
@ws&.close
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def channel(name)
|
|
291
|
+
@channels[name] ||= Channel.new(name, method(:send_msg))
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def configure_push(base_url:, token:, app_id:)
|
|
295
|
+
PushClient.new(base_url: base_url, token: token, app_id: app_id)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private
|
|
299
|
+
|
|
300
|
+
def send_msg(msg)
|
|
301
|
+
return unless @connected
|
|
302
|
+
|
|
303
|
+
hash = stringify_keys(msg)
|
|
304
|
+
@ws&.send(hash.to_json)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def handle_open
|
|
308
|
+
@connected = true
|
|
309
|
+
@reconnect_attempts = 0
|
|
310
|
+
|
|
311
|
+
if @options.recover && !@subscribed_channels.empty? && @last_message_ts > 0
|
|
312
|
+
resume_data = { channels: @subscribed_channels.keys, since: @last_message_ts }
|
|
313
|
+
token = Base64.urlsafe_encode64(resume_data.to_json, padding: false)
|
|
314
|
+
send_msg({ action: 'resume', token: token })
|
|
315
|
+
else
|
|
316
|
+
@subscribed_channels.each_key do |ch|
|
|
317
|
+
send_msg({ action: 'subscribe', channel: ch })
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
@on_connect_cbs.each(&:call)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def handle_raw_message(raw)
|
|
325
|
+
msg = JSON.parse(raw)
|
|
326
|
+
action = msg['action']
|
|
327
|
+
return unless action
|
|
328
|
+
|
|
329
|
+
channel_name = msg['channel']
|
|
330
|
+
|
|
331
|
+
case action
|
|
332
|
+
when 'message'
|
|
333
|
+
ch = channel_name && @channels[channel_name]
|
|
334
|
+
return unless ch
|
|
335
|
+
|
|
336
|
+
ts = msg['timestamp']&.to_i || (Time.now.to_f * 1000).to_i
|
|
337
|
+
@last_message_ts = ts if ts > @last_message_ts
|
|
338
|
+
meta = MessageMeta.new(id: msg['id'] || '', channel: channel_name, timestamp: ts)
|
|
339
|
+
ch.handle_message(msg['data'], meta)
|
|
340
|
+
|
|
341
|
+
when 'subscribed'
|
|
342
|
+
@subscribed_channels[channel_name] = true if channel_name
|
|
343
|
+
|
|
344
|
+
when 'unsubscribed'
|
|
345
|
+
@subscribed_channels.delete(channel_name) if channel_name
|
|
346
|
+
|
|
347
|
+
when 'history'
|
|
348
|
+
ch = channel_name && @channels[channel_name]
|
|
349
|
+
return unless ch
|
|
350
|
+
|
|
351
|
+
messages = (msg['messages'] || []).map do |m|
|
|
352
|
+
HistoryMessage.new(
|
|
353
|
+
id: m['id'] || '', channel: channel_name,
|
|
354
|
+
data: m['data'], publisher_id: m['publisherId'] || '',
|
|
355
|
+
timestamp: m['timestamp']&.to_i || 0,
|
|
356
|
+
sequence: m['sequence']&.to_i || 0
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
ch.handle_history(HistoryResult.new(channel: channel_name, messages: messages, has_more: msg['hasMore'] == true))
|
|
360
|
+
|
|
361
|
+
when 'presence.enter', 'presence.leave', 'presence.update', 'presence.members'
|
|
362
|
+
ch = channel_name && @channels[channel_name]
|
|
363
|
+
return unless ch
|
|
364
|
+
|
|
365
|
+
ch.presence.handle_event(action, msg)
|
|
366
|
+
|
|
367
|
+
when 'error'
|
|
368
|
+
err = msg['error'] || 'Unknown error'
|
|
369
|
+
@on_error_cbs.each { |cb| cb.call(RuntimeError.new(err)) }
|
|
370
|
+
end
|
|
371
|
+
rescue StandardError => e
|
|
372
|
+
@on_error_cbs.each { |cb| cb.call(e) }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def handle_close(_event)
|
|
376
|
+
@connected = false
|
|
377
|
+
@on_disconnect_cbs.each { |cb| cb.call(1000) }
|
|
378
|
+
maybe_reconnect
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def handle_error(error)
|
|
382
|
+
@connected = false
|
|
383
|
+
@on_error_cbs.each { |cb| cb.call(error) }
|
|
384
|
+
maybe_reconnect
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def maybe_reconnect
|
|
388
|
+
return unless @options.auto_reconnect
|
|
389
|
+
return if @reconnect_attempts >= @options.max_reconnect_attempts
|
|
390
|
+
|
|
391
|
+
@reconnect_attempts += 1
|
|
392
|
+
delay = @options.reconnect_delay * @reconnect_attempts
|
|
393
|
+
Thread.new do
|
|
394
|
+
sleep(delay)
|
|
395
|
+
connect unless @connected
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def stringify_keys(hash)
|
|
400
|
+
hash.transform_keys(&:to_s).transform_values do |v|
|
|
401
|
+
v.is_a?(Hash) ? stringify_keys(v) : v
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wsocket-io
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- wSocket
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: websocket-client-simple
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.8'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.8'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: json
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.6'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.6'
|
|
41
|
+
description: Official Ruby SDK for wSocket — realtime pub/sub, presence, history,
|
|
42
|
+
and push notifications.
|
|
43
|
+
email: sdk@wsocket.io
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/wsocket_io.rb
|
|
51
|
+
homepage: https://github.com/wsocket-io/sdk-ruby
|
|
52
|
+
licenses:
|
|
53
|
+
- MIT
|
|
54
|
+
metadata: {}
|
|
55
|
+
post_install_message:
|
|
56
|
+
rdoc_options: []
|
|
57
|
+
require_paths:
|
|
58
|
+
- lib
|
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '3.0'
|
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
requirements: []
|
|
70
|
+
rubygems_version: 3.3.5
|
|
71
|
+
signing_key:
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: wSocket SDK for Ruby
|
|
74
|
+
test_files: []
|