solrengine-realtime 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e24339e63d6e1bea90aa6dd8bac526f5ef32659451ea97866b4b2d90f927b171
4
+ data.tar.gz: c64312b31bd91b27311431729677109b22a9dee0de8e38a3b8784e3cbff2352d
5
+ SHA512:
6
+ metadata.gz: 4bb5d4ecb5d4bdf28d9d90c7308506bbd17cda2ab4bed4f0f7534b9fe72f68aeaff195c87b329bb7c6af3d0b46ae7bb560c58d5405f1429da0db68699461b25b
7
+ data.tar.gz: 2b610121748e95b28ce407e363bdb7221708f8f8b1ba87ac4558f0a4133e2f2e283cc428f5d30585c4450397d5be6b262409d703056385e3dcd212fefb09fd93
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # SolRengine Realtime
2
+
3
+ Real-time Solana account monitoring via WebSocket + Turbo Streams. Subscribes to account changes and triggers callbacks when balances update.
4
+
5
+ Part of the [SolRengine](https://github.com/solrengine) framework.
6
+
7
+ ## Install
8
+
9
+ ```ruby
10
+ gem "solrengine-realtime"
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Configure what happens when an account changes:
16
+
17
+ ```ruby
18
+ # config/initializers/solrengine_realtime.rb
19
+ Solrengine::Realtime.on_account_change = ->(wallet_address) {
20
+ Rails.cache.delete("wallet/#{wallet_address}/tokens")
21
+
22
+ portfolio = Solrengine::Tokens::Portfolio.new(wallet_address)
23
+
24
+ Turbo::StreamsChannel.broadcast_replace_to(
25
+ "wallet_#{wallet_address}",
26
+ target: "token_list",
27
+ partial: "dashboard/token_list",
28
+ locals: { tokens: portfolio.tokens }
29
+ )
30
+ }
31
+ ```
32
+
33
+ Run the monitor as a separate process:
34
+
35
+ ```ruby
36
+ # bin/solana_monitor
37
+ monitor = Solrengine::Realtime::AccountMonitor.new("Abc...xyz")
38
+ monitor.start
39
+ ```
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,46 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { morphChildren } from "@hotwired/turbo"
3
+
4
+ // Periodically fetches fresh HTML and morphs only the changed DOM elements.
5
+ // No Turbo visit, no loading bar, no scroll reset — completely invisible.
6
+ export default class extends Controller {
7
+ static values = {
8
+ interval: { type: Number, default: 60 }
9
+ }
10
+
11
+ connect() {
12
+ this.timer = setInterval(() => {
13
+ if (document.visibilityState === "visible") {
14
+ this.morph()
15
+ }
16
+ }, this.intervalValue * 1000)
17
+ }
18
+
19
+ disconnect() {
20
+ clearInterval(this.timer)
21
+ }
22
+
23
+ async morph() {
24
+ if (this._morphing) return
25
+ this._morphing = true
26
+
27
+ try {
28
+ const response = await fetch(window.location.href, {
29
+ headers: { "Accept": "text/html" }
30
+ })
31
+ if (!response.ok) return
32
+
33
+ const html = await response.text()
34
+ const doc = new DOMParser().parseFromString(html, "text/html")
35
+ const newContent = doc.querySelector(`[data-controller~="auto-refresh"]`)
36
+
37
+ if (newContent) {
38
+ morphChildren(this.element, newContent)
39
+ }
40
+ } catch {
41
+ // Silently fail — next interval will retry
42
+ } finally {
43
+ this._morphing = false
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,148 @@
1
+ require "json"
2
+ require "logger"
3
+ require "websocket-client-simple"
4
+
5
+ module Solrengine
6
+ module Realtime
7
+ # Subscribes to Solana WebSocket RPC for real-time account changes.
8
+ # When an account changes, calls Solrengine::Realtime.on_account_change.
9
+ class AccountMonitor
10
+ SOLANA_ADDRESS_REGEX = /\A[1-9A-HJ-NP-Za-km-z]{32,44}\z/
11
+ RECONNECT_DELAY = 5
12
+ BROADCAST_DELAY = 3
13
+
14
+ attr_reader :wallet_address
15
+
16
+ def initialize(wallet_address)
17
+ unless wallet_address.is_a?(String) && wallet_address.match?(SOLANA_ADDRESS_REGEX)
18
+ raise ArgumentError, "Invalid Solana wallet address"
19
+ end
20
+
21
+ @wallet_address = wallet_address.freeze
22
+ @ws_url = Solrengine::Rpc.configuration.ws_url
23
+
24
+ unless @ws_url&.start_with?("wss://")
25
+ logger.warn("[SolanaWS] WebSocket URL does not use wss:// (TLS) — connection is unencrypted")
26
+ end
27
+
28
+ @running = false
29
+ @subscription_id = nil
30
+ @account_changed = false
31
+ @mutex = Mutex.new
32
+ @cv = ConditionVariable.new
33
+ end
34
+
35
+ def start
36
+ return if @running
37
+ @running = true
38
+
39
+ @ws_thread = Thread.new { websocket_loop }
40
+ @broadcast_thread = Thread.new { broadcast_loop }
41
+ end
42
+
43
+ def stop
44
+ @running = false
45
+ @cv.signal
46
+ @ws&.close rescue nil
47
+ @ws_thread&.join(5)
48
+ @broadcast_thread&.join(5)
49
+ @ws_thread = nil
50
+ @broadcast_thread = nil
51
+ end
52
+
53
+ def running?
54
+ @running
55
+ end
56
+
57
+ def flag_changed!
58
+ @mutex.synchronize do
59
+ @account_changed = true
60
+ @cv.signal
61
+ end
62
+ end
63
+
64
+ def logger
65
+ @logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout, progname: "SolanaWS")
66
+ end
67
+
68
+ private
69
+
70
+ def websocket_loop
71
+ while @running
72
+ begin
73
+ connect_and_listen
74
+ rescue => e
75
+ logger.error("[SolanaWS] Connection error: #{e.message}")
76
+ sleep RECONNECT_DELAY if @running
77
+ end
78
+ end
79
+ end
80
+
81
+ def connect_and_listen
82
+ @ws = WebSocket::Client::Simple.connect(@ws_url)
83
+ ws = @ws
84
+ monitor = self
85
+
86
+ ws.on :open do
87
+ monitor.logger.info("[SolanaWS] Connected for #{monitor.wallet_address} on #{Solrengine::Rpc.configuration.network}")
88
+ ws.send({
89
+ jsonrpc: "2.0",
90
+ id: 1,
91
+ method: "accountSubscribe",
92
+ params: [
93
+ monitor.wallet_address,
94
+ { encoding: "jsonParsed", commitment: "confirmed" }
95
+ ]
96
+ }.to_json)
97
+ end
98
+
99
+ ws.on :message do |msg|
100
+ begin
101
+ parsed = JSON.parse(msg.data)
102
+ if parsed["id"] == 1 && parsed["result"]
103
+ monitor.instance_variable_set(:@subscription_id, parsed["result"])
104
+ monitor.logger.info("[SolanaWS] Subscribed with ID #{parsed['result']}")
105
+ elsif parsed["method"] == "accountNotification" &&
106
+ parsed.dig("params", "subscription") == monitor.instance_variable_get(:@subscription_id)
107
+ monitor.logger.info("[SolanaWS] Account changed for #{monitor.wallet_address}")
108
+ monitor.flag_changed!
109
+ end
110
+ rescue => e
111
+ monitor.logger.error("[SolanaWS] Message parse error: #{e.message}")
112
+ end
113
+ end
114
+
115
+ ws.on :error do |e|
116
+ monitor.logger.error("[SolanaWS] Error: #{e.message}")
117
+ end
118
+
119
+ ws.on :close do
120
+ monitor.logger.info("[SolanaWS] Disconnected")
121
+ end
122
+
123
+ sleep 1 while @running && !ws.closed?
124
+ end
125
+
126
+ def broadcast_loop
127
+ while @running
128
+ changed = @mutex.synchronize do
129
+ @cv.wait(@mutex, 1) until @account_changed || !@running
130
+ val = @account_changed
131
+ @account_changed = false
132
+ val
133
+ end
134
+
135
+ if changed && @running
136
+ sleep BROADCAST_DELAY
137
+ @mutex.synchronize { @account_changed = false }
138
+ begin
139
+ Solrengine::Realtime.on_account_change.call(@wallet_address)
140
+ rescue => e
141
+ logger.error("[SolanaWS] Callback error: #{e.message}")
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,11 @@
1
+ module Solrengine
2
+ module Realtime
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Solrengine::Realtime
5
+
6
+ initializer "solrengine-realtime.assets" do |app|
7
+ app.config.assets.paths << root.join("app/assets/javascripts")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solrengine
4
+ module Realtime
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ require "active_support/core_ext/module/attribute_accessors"
2
+ require "solrengine/rpc"
3
+ require_relative "realtime/version"
4
+ require_relative "realtime/account_monitor"
5
+ require_relative "realtime/engine" if defined?(Rails::Engine)
6
+
7
+ module Solrengine
8
+ module Realtime
9
+ mattr_accessor :on_account_change
10
+
11
+ self.on_account_change = ->(wallet_address) {
12
+ puts "[SolRengine] Account changed: #{wallet_address}"
13
+ }
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solrengine-realtime
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jose Ferrer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: solrengine-rpc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: websocket-client-simple
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.9'
55
+ description: Subscribes to Solana WebSocket RPC for account changes and broadcasts
56
+ updates via ActionCable/Turbo Streams. Includes Idiomorph auto-refresh Stimulus
57
+ controller.
58
+ email:
59
+ - estoy@moviendo.me
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - README.md
65
+ - app/assets/javascripts/solrengine/realtime/auto_refresh_controller.js
66
+ - lib/solrengine/realtime.rb
67
+ - lib/solrengine/realtime/account_monitor.rb
68
+ - lib/solrengine/realtime/engine.rb
69
+ - lib/solrengine/realtime/version.rb
70
+ homepage: https://github.com/solrengine/realtime
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/solrengine/realtime
75
+ source_code_uri: https://github.com/solrengine/realtime
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 3.2.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.5.22
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Real-time Solana account monitoring via WebSocket + Turbo Streams
95
+ test_files: []