parse-stack-next 4.5.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/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "monitor"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module LiveQuery
|
|
8
|
+
# Monitors WebSocket connection health via ping/pong and activity tracking.
|
|
9
|
+
#
|
|
10
|
+
# Schedules periodic ping frames and detects stale connections when pong
|
|
11
|
+
# responses are not received within the configured timeout.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# monitor = HealthMonitor.new(client: client, ping_interval: 30.0, pong_timeout: 10.0)
|
|
15
|
+
# monitor.start
|
|
16
|
+
# # ... connection activity ...
|
|
17
|
+
# monitor.stop
|
|
18
|
+
#
|
|
19
|
+
class HealthMonitor
|
|
20
|
+
# Default ping interval in seconds
|
|
21
|
+
DEFAULT_PING_INTERVAL = 30.0
|
|
22
|
+
|
|
23
|
+
# Default pong timeout in seconds
|
|
24
|
+
DEFAULT_PONG_TIMEOUT = 10.0
|
|
25
|
+
|
|
26
|
+
# @return [Float] seconds between ping frames
|
|
27
|
+
attr_reader :ping_interval
|
|
28
|
+
|
|
29
|
+
# @return [Float] seconds to wait for pong response
|
|
30
|
+
attr_reader :pong_timeout
|
|
31
|
+
|
|
32
|
+
# @return [Time, nil] when connection was established
|
|
33
|
+
attr_reader :connection_established_at
|
|
34
|
+
|
|
35
|
+
# @return [Time, nil] last activity (any message received)
|
|
36
|
+
attr_reader :last_activity_at
|
|
37
|
+
|
|
38
|
+
# @return [Time, nil] last pong received
|
|
39
|
+
attr_reader :last_pong_at
|
|
40
|
+
|
|
41
|
+
# Create a new health monitor
|
|
42
|
+
# @param client [Client] the LiveQuery client to monitor
|
|
43
|
+
# @param ping_interval [Float] seconds between pings
|
|
44
|
+
# @param pong_timeout [Float] seconds to wait for pong
|
|
45
|
+
def initialize(client:, ping_interval: DEFAULT_PING_INTERVAL, pong_timeout: DEFAULT_PONG_TIMEOUT)
|
|
46
|
+
@client = client
|
|
47
|
+
@ping_interval = ping_interval
|
|
48
|
+
@pong_timeout = pong_timeout
|
|
49
|
+
|
|
50
|
+
@monitor = Monitor.new
|
|
51
|
+
@running = false
|
|
52
|
+
@ping_thread = nil
|
|
53
|
+
@awaiting_pong = false
|
|
54
|
+
|
|
55
|
+
@connection_established_at = nil
|
|
56
|
+
@last_activity_at = nil
|
|
57
|
+
@last_pong_at = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Start the health monitoring thread
|
|
61
|
+
# @return [void]
|
|
62
|
+
def start
|
|
63
|
+
@monitor.synchronize do
|
|
64
|
+
return if @running
|
|
65
|
+
|
|
66
|
+
@running = true
|
|
67
|
+
@connection_established_at = Time.now
|
|
68
|
+
@last_activity_at = Time.now
|
|
69
|
+
@last_pong_at = Time.now
|
|
70
|
+
@awaiting_pong = false
|
|
71
|
+
|
|
72
|
+
@ping_thread = Thread.new { ping_loop }
|
|
73
|
+
@ping_thread.abort_on_exception = false
|
|
74
|
+
|
|
75
|
+
Logging.debug("Health monitor started", ping_interval: @ping_interval, pong_timeout: @pong_timeout)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Stop the health monitoring thread
|
|
80
|
+
# @return [void]
|
|
81
|
+
def stop
|
|
82
|
+
@monitor.synchronize do
|
|
83
|
+
return unless @running
|
|
84
|
+
|
|
85
|
+
@running = false
|
|
86
|
+
@ping_thread&.kill
|
|
87
|
+
@ping_thread = nil
|
|
88
|
+
@awaiting_pong = false
|
|
89
|
+
|
|
90
|
+
Logging.debug("Health monitor stopped")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Record that a pong was received
|
|
95
|
+
# @return [void]
|
|
96
|
+
def record_pong
|
|
97
|
+
@monitor.synchronize do
|
|
98
|
+
@last_pong_at = Time.now
|
|
99
|
+
@last_activity_at = Time.now
|
|
100
|
+
@awaiting_pong = false
|
|
101
|
+
end
|
|
102
|
+
Logging.debug("Pong received")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Record that activity was received (any message)
|
|
106
|
+
# @return [void]
|
|
107
|
+
def record_activity
|
|
108
|
+
@monitor.synchronize do
|
|
109
|
+
@last_activity_at = Time.now
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if monitor is running
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def running?
|
|
116
|
+
@monitor.synchronize { @running }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if connection is stale (no pong within timeout)
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def stale?
|
|
122
|
+
@monitor.synchronize do
|
|
123
|
+
return false unless @awaiting_pong
|
|
124
|
+
return false unless @last_pong_at
|
|
125
|
+
|
|
126
|
+
Time.now - @last_pong_at > (@ping_interval + @pong_timeout)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if connection appears healthy
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def healthy?
|
|
133
|
+
@monitor.synchronize do
|
|
134
|
+
return false unless @running
|
|
135
|
+
return true unless @last_activity_at
|
|
136
|
+
|
|
137
|
+
# Consider unhealthy if no activity for 2x ping interval + pong timeout
|
|
138
|
+
max_idle = (@ping_interval * 2) + @pong_timeout
|
|
139
|
+
Time.now - @last_activity_at < max_idle
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Seconds since last activity
|
|
144
|
+
# @return [Float, nil]
|
|
145
|
+
def seconds_since_activity
|
|
146
|
+
@monitor.synchronize do
|
|
147
|
+
return nil unless @last_activity_at
|
|
148
|
+
Time.now - @last_activity_at
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Seconds since last pong
|
|
153
|
+
# @return [Float, nil]
|
|
154
|
+
def seconds_since_pong
|
|
155
|
+
@monitor.synchronize do
|
|
156
|
+
return nil unless @last_pong_at
|
|
157
|
+
Time.now - @last_pong_at
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get health information as a hash
|
|
162
|
+
# @return [Hash]
|
|
163
|
+
def health_info
|
|
164
|
+
@monitor.synchronize do
|
|
165
|
+
{
|
|
166
|
+
running: @running,
|
|
167
|
+
healthy: healthy?,
|
|
168
|
+
stale: stale?,
|
|
169
|
+
awaiting_pong: @awaiting_pong,
|
|
170
|
+
connection_established_at: @connection_established_at,
|
|
171
|
+
last_activity_at: @last_activity_at,
|
|
172
|
+
last_pong_at: @last_pong_at,
|
|
173
|
+
seconds_since_activity: seconds_since_activity,
|
|
174
|
+
seconds_since_pong: seconds_since_pong,
|
|
175
|
+
ping_interval: @ping_interval,
|
|
176
|
+
pong_timeout: @pong_timeout,
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
# Main ping loop - runs in background thread
|
|
184
|
+
def ping_loop
|
|
185
|
+
while @running
|
|
186
|
+
begin
|
|
187
|
+
sleep @ping_interval
|
|
188
|
+
break unless @running
|
|
189
|
+
|
|
190
|
+
# Send ping and mark as awaiting pong
|
|
191
|
+
@monitor.synchronize { @awaiting_pong = true }
|
|
192
|
+
|
|
193
|
+
Logging.debug("Sending ping")
|
|
194
|
+
@client.send(:send_ping)
|
|
195
|
+
|
|
196
|
+
# Wait for pong timeout
|
|
197
|
+
sleep @pong_timeout
|
|
198
|
+
break unless @running
|
|
199
|
+
|
|
200
|
+
# Check if pong was received
|
|
201
|
+
if @awaiting_pong
|
|
202
|
+
Logging.warn("Connection stale: no pong received", seconds_waited: @ping_interval + @pong_timeout)
|
|
203
|
+
@client.send(:handle_stale_connection)
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
rescue StandardError => e
|
|
207
|
+
Logging.error("Ping loop error", error: e)
|
|
208
|
+
break
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "logger"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module LiveQuery
|
|
8
|
+
# Structured logging module for LiveQuery.
|
|
9
|
+
#
|
|
10
|
+
# Provides leveled logging with context support. Disabled by default.
|
|
11
|
+
#
|
|
12
|
+
# @example Enable logging
|
|
13
|
+
# Parse::LiveQuery::Logging.enabled = true
|
|
14
|
+
# Parse::LiveQuery::Logging.log_level = :debug
|
|
15
|
+
#
|
|
16
|
+
# @example Use custom logger
|
|
17
|
+
# Parse::LiveQuery::Logging.logger = Rails.logger
|
|
18
|
+
#
|
|
19
|
+
module Logging
|
|
20
|
+
# Log levels in order of verbosity
|
|
21
|
+
LEVELS = [:debug, :info, :warn, :error].freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# @return [Boolean] whether logging is enabled
|
|
25
|
+
attr_accessor :enabled
|
|
26
|
+
|
|
27
|
+
# @return [Logger, nil] custom logger instance
|
|
28
|
+
attr_accessor :logger
|
|
29
|
+
|
|
30
|
+
# @return [Symbol] current log level (:debug, :info, :warn, :error)
|
|
31
|
+
attr_reader :log_level
|
|
32
|
+
|
|
33
|
+
# Set log level with validation
|
|
34
|
+
# @param level [Symbol] one of :debug, :info, :warn, :error
|
|
35
|
+
def log_level=(level)
|
|
36
|
+
unless LEVELS.include?(level)
|
|
37
|
+
raise ArgumentError, "Invalid log level: #{level}. Must be one of #{LEVELS.inspect}"
|
|
38
|
+
end
|
|
39
|
+
@log_level = level
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get or create the default logger
|
|
43
|
+
# @return [Logger]
|
|
44
|
+
def default_logger
|
|
45
|
+
@default_logger ||= begin
|
|
46
|
+
l = ::Logger.new($stdout)
|
|
47
|
+
l.progname = "Parse::LiveQuery"
|
|
48
|
+
l.formatter = proc do |severity, datetime, progname, msg|
|
|
49
|
+
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S")}] #{severity} -- #{progname}: #{msg}\n"
|
|
50
|
+
end
|
|
51
|
+
l
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get the current logger (custom or default)
|
|
56
|
+
# @return [Logger]
|
|
57
|
+
def current_logger
|
|
58
|
+
logger || default_logger
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Log a debug message
|
|
62
|
+
# @param message [String] the message
|
|
63
|
+
# @param context [Hash] optional context data
|
|
64
|
+
def debug(message, **context)
|
|
65
|
+
log(:debug, message, context)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Log an info message
|
|
69
|
+
# @param message [String] the message
|
|
70
|
+
# @param context [Hash] optional context data
|
|
71
|
+
def info(message, **context)
|
|
72
|
+
log(:info, message, context)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Log a warning message
|
|
76
|
+
# @param message [String] the message
|
|
77
|
+
# @param context [Hash] optional context data
|
|
78
|
+
def warn(message, **context)
|
|
79
|
+
log(:warn, message, context)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Log an error message
|
|
83
|
+
# @param message [String] the message
|
|
84
|
+
# @param context [Hash] optional context data
|
|
85
|
+
def error(message, **context)
|
|
86
|
+
log(:error, message, context)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Reset logging configuration to defaults
|
|
90
|
+
def reset!
|
|
91
|
+
@enabled = false
|
|
92
|
+
@logger = nil
|
|
93
|
+
@log_level = :info
|
|
94
|
+
@default_logger = nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
# Check if a level should be logged based on current log_level
|
|
100
|
+
# @param level [Symbol] the level to check
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def should_log?(level)
|
|
103
|
+
return false unless enabled
|
|
104
|
+
|
|
105
|
+
current_level_index = LEVELS.index(@log_level || :info)
|
|
106
|
+
message_level_index = LEVELS.index(level)
|
|
107
|
+
message_level_index >= current_level_index
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Internal log method
|
|
111
|
+
# @param level [Symbol] log level
|
|
112
|
+
# @param message [String] the message
|
|
113
|
+
# @param context [Hash] context data
|
|
114
|
+
def log(level, message, context)
|
|
115
|
+
return unless should_log?(level)
|
|
116
|
+
|
|
117
|
+
formatted = if context.any?
|
|
118
|
+
"#{message} #{format_context(context)}"
|
|
119
|
+
else
|
|
120
|
+
message
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
current_logger.send(level, formatted)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Format context hash for logging
|
|
127
|
+
# @param context [Hash] context data
|
|
128
|
+
# @return [String]
|
|
129
|
+
def format_context(context)
|
|
130
|
+
context.map do |k, v|
|
|
131
|
+
value = case v
|
|
132
|
+
when Exception
|
|
133
|
+
"#{v.class}: #{v.message}"
|
|
134
|
+
when String
|
|
135
|
+
v.length > 100 ? "#{v[0..97]}..." : v
|
|
136
|
+
else
|
|
137
|
+
v.inspect
|
|
138
|
+
end
|
|
139
|
+
"#{k}=#{value}"
|
|
140
|
+
end.join(" ")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Initialize defaults
|
|
145
|
+
@enabled = false
|
|
146
|
+
@log_level = :info
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "monitor"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module LiveQuery
|
|
8
|
+
# Represents an active subscription to a LiveQuery.
|
|
9
|
+
# Manages event callbacks and subscription lifecycle.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# subscription = Song.subscribe(where: { artist: "Beatles" })
|
|
13
|
+
#
|
|
14
|
+
# # Register callbacks using on() method
|
|
15
|
+
# subscription.on(:create) { |song| puts "New song!" }
|
|
16
|
+
# subscription.on(:update) { |song, original| puts "Updated!" }
|
|
17
|
+
#
|
|
18
|
+
# # Or use shorthand methods
|
|
19
|
+
# subscription.on_create { |song| puts "New song!" }
|
|
20
|
+
# subscription.on_update { |song, original| puts "Updated!" }
|
|
21
|
+
# subscription.on_delete { |song| puts "Deleted!" }
|
|
22
|
+
# subscription.on_enter { |song, original| puts "Entered query!" }
|
|
23
|
+
# subscription.on_leave { |song, original| puts "Left query!" }
|
|
24
|
+
#
|
|
25
|
+
# # Error handling
|
|
26
|
+
# subscription.on_error { |error| puts "Error: #{error.message}" }
|
|
27
|
+
#
|
|
28
|
+
# # Connection events
|
|
29
|
+
# subscription.on_subscribe { puts "Subscribed!" }
|
|
30
|
+
# subscription.on_unsubscribe { puts "Unsubscribed!" }
|
|
31
|
+
#
|
|
32
|
+
# # Cleanup
|
|
33
|
+
# subscription.unsubscribe
|
|
34
|
+
#
|
|
35
|
+
class Subscription
|
|
36
|
+
# Class-level monitor for request ID generation
|
|
37
|
+
@@id_monitor = Monitor.new
|
|
38
|
+
@@request_counter = 0
|
|
39
|
+
|
|
40
|
+
# @return [Integer] unique request ID for this subscription
|
|
41
|
+
attr_reader :request_id
|
|
42
|
+
|
|
43
|
+
# @return [String] Parse class name being subscribed to
|
|
44
|
+
attr_reader :class_name
|
|
45
|
+
|
|
46
|
+
# @return [Hash] the query constraints (where clause)
|
|
47
|
+
attr_reader :query
|
|
48
|
+
|
|
49
|
+
# @return [Parse::LiveQuery::Client] the LiveQuery client
|
|
50
|
+
attr_reader :client
|
|
51
|
+
|
|
52
|
+
# @return [Array<String>] fields to watch for changes (nil = all fields)
|
|
53
|
+
attr_reader :fields
|
|
54
|
+
|
|
55
|
+
# @return [String, nil] session token for ACL-aware subscriptions
|
|
56
|
+
attr_reader :session_token
|
|
57
|
+
|
|
58
|
+
# Create a new subscription
|
|
59
|
+
# @param client [Parse::LiveQuery::Client] the LiveQuery client
|
|
60
|
+
# @param class_name [String] Parse class name
|
|
61
|
+
# @param query [Hash] query constraints (where clause)
|
|
62
|
+
# @param fields [Array<String>, nil] specific fields to watch
|
|
63
|
+
# @param session_token [String, nil] session token for authentication
|
|
64
|
+
def initialize(client:, class_name:, query: {}, fields: nil, session_token: nil)
|
|
65
|
+
@monitor = Monitor.new
|
|
66
|
+
@client = client
|
|
67
|
+
@class_name = class_name
|
|
68
|
+
@query = query
|
|
69
|
+
@fields = fields
|
|
70
|
+
@session_token = session_token
|
|
71
|
+
@request_id = generate_request_id
|
|
72
|
+
@state = :pending
|
|
73
|
+
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
74
|
+
|
|
75
|
+
Logging.debug("Subscription created",
|
|
76
|
+
request_id: @request_id,
|
|
77
|
+
class_name: @class_name,
|
|
78
|
+
query_keys: @query.keys)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Current subscription state
|
|
82
|
+
# @return [Symbol] :pending, :subscribed, :unsubscribed, or :error
|
|
83
|
+
def state
|
|
84
|
+
@monitor.synchronize { @state }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Register a callback for a specific event type
|
|
88
|
+
# @param event_type [Symbol] :create, :update, :delete, :enter, :leave, :error, :subscribe, :unsubscribe
|
|
89
|
+
# @yield [object, original] block to call when event occurs
|
|
90
|
+
# @return [self]
|
|
91
|
+
def on(event_type, &block)
|
|
92
|
+
return self unless block_given?
|
|
93
|
+
|
|
94
|
+
@monitor.synchronize do
|
|
95
|
+
@callbacks[event_type.to_sym] << block
|
|
96
|
+
end
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Register callback for create events
|
|
101
|
+
# @yield [Parse::Object] the created object
|
|
102
|
+
# @return [self]
|
|
103
|
+
def on_create(&block)
|
|
104
|
+
on(:create, &block)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Register callback for update events
|
|
108
|
+
# @yield [Parse::Object, Parse::Object] updated object, original object
|
|
109
|
+
# @return [self]
|
|
110
|
+
def on_update(&block)
|
|
111
|
+
on(:update, &block)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Register callback for delete events
|
|
115
|
+
# @yield [Parse::Object] the deleted object
|
|
116
|
+
# @return [self]
|
|
117
|
+
def on_delete(&block)
|
|
118
|
+
on(:delete, &block)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Register callback for enter events (object now matches query)
|
|
122
|
+
# @yield [Parse::Object, Parse::Object] current object, original object
|
|
123
|
+
# @return [self]
|
|
124
|
+
def on_enter(&block)
|
|
125
|
+
on(:enter, &block)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Register callback for leave events (object no longer matches query)
|
|
129
|
+
# @yield [Parse::Object, Parse::Object] current object, original object
|
|
130
|
+
# @return [self]
|
|
131
|
+
def on_leave(&block)
|
|
132
|
+
on(:leave, &block)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Register callback for errors
|
|
136
|
+
# @yield [Exception] the error that occurred
|
|
137
|
+
# @return [self]
|
|
138
|
+
def on_error(&block)
|
|
139
|
+
on(:error, &block)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Register callback for successful subscription
|
|
143
|
+
# @yield called when subscription is confirmed
|
|
144
|
+
# @return [self]
|
|
145
|
+
def on_subscribe(&block)
|
|
146
|
+
on(:subscribe, &block)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Register callback for unsubscription
|
|
150
|
+
# @yield called when unsubscribed
|
|
151
|
+
# @return [self]
|
|
152
|
+
def on_unsubscribe(&block)
|
|
153
|
+
on(:unsubscribe, &block)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Unsubscribe from this subscription
|
|
157
|
+
# @return [Boolean] true if unsubscribe message was sent
|
|
158
|
+
def unsubscribe
|
|
159
|
+
@monitor.synchronize do
|
|
160
|
+
return false if @state == :unsubscribed
|
|
161
|
+
@state = :unsubscribed
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
Logging.debug("Unsubscribing", request_id: @request_id)
|
|
165
|
+
client.unsubscribe(self)
|
|
166
|
+
emit(:unsubscribe)
|
|
167
|
+
true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @return [Boolean] true if currently subscribed
|
|
171
|
+
def subscribed?
|
|
172
|
+
state == :subscribed
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @return [Boolean] true if pending subscription confirmation
|
|
176
|
+
def pending?
|
|
177
|
+
state == :pending
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @return [Boolean] true if unsubscribed
|
|
181
|
+
def unsubscribed?
|
|
182
|
+
state == :unsubscribed
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @return [Boolean] true if in error state
|
|
186
|
+
def error?
|
|
187
|
+
state == :error
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Build the subscription message to send to the server
|
|
191
|
+
# @return [Hash]
|
|
192
|
+
def to_subscribe_message
|
|
193
|
+
msg = {
|
|
194
|
+
op: "subscribe",
|
|
195
|
+
requestId: request_id,
|
|
196
|
+
query: {
|
|
197
|
+
className: class_name,
|
|
198
|
+
where: query,
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
msg[:query][:fields] = fields if fields&.any?
|
|
203
|
+
msg[:sessionToken] = session_token if session_token
|
|
204
|
+
|
|
205
|
+
msg
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Build the unsubscribe message
|
|
209
|
+
# @return [Hash]
|
|
210
|
+
def to_unsubscribe_message
|
|
211
|
+
{
|
|
212
|
+
op: "unsubscribe",
|
|
213
|
+
requestId: request_id,
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Handle an incoming event from the server
|
|
218
|
+
# @param event [Parse::LiveQuery::Event]
|
|
219
|
+
# @api private
|
|
220
|
+
def handle_event(event)
|
|
221
|
+
Logging.debug("Handling event",
|
|
222
|
+
request_id: @request_id,
|
|
223
|
+
event_type: event.type)
|
|
224
|
+
emit(event.type, event.object, event.original)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Mark subscription as confirmed by server
|
|
228
|
+
# @api private
|
|
229
|
+
def confirm!
|
|
230
|
+
@monitor.synchronize { @state = :subscribed }
|
|
231
|
+
Logging.info("Subscription confirmed",
|
|
232
|
+
request_id: @request_id,
|
|
233
|
+
class_name: @class_name)
|
|
234
|
+
emit(:subscribe)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Mark subscription as failed with error
|
|
238
|
+
# @param error [Exception, String]
|
|
239
|
+
# @api private
|
|
240
|
+
def fail!(error)
|
|
241
|
+
@monitor.synchronize { @state = :error }
|
|
242
|
+
error = SubscriptionError.new(error) if error.is_a?(String)
|
|
243
|
+
Logging.error("Subscription failed",
|
|
244
|
+
request_id: @request_id,
|
|
245
|
+
error: error)
|
|
246
|
+
emit(:error, error)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @return [Hash] subscription info as hash
|
|
250
|
+
def to_h
|
|
251
|
+
@monitor.synchronize do
|
|
252
|
+
{
|
|
253
|
+
request_id: request_id,
|
|
254
|
+
class_name: class_name,
|
|
255
|
+
query: query,
|
|
256
|
+
state: @state,
|
|
257
|
+
fields: fields,
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
|
|
264
|
+
# Emit an event to registered callbacks
|
|
265
|
+
# @param event_type [Symbol]
|
|
266
|
+
# @param args [Array] arguments to pass to callbacks
|
|
267
|
+
def emit(event_type, *args)
|
|
268
|
+
# Copy callbacks under lock, iterate outside to prevent deadlocks
|
|
269
|
+
callbacks = @monitor.synchronize { @callbacks[event_type].dup }
|
|
270
|
+
|
|
271
|
+
callbacks.each do |callback|
|
|
272
|
+
begin
|
|
273
|
+
callback.call(*args)
|
|
274
|
+
rescue => e
|
|
275
|
+
# Don't let callback errors break the subscription
|
|
276
|
+
Logging.error("Callback error",
|
|
277
|
+
request_id: @request_id,
|
|
278
|
+
event_type: event_type,
|
|
279
|
+
error: e)
|
|
280
|
+
emit(:error, e) unless event_type == :error
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Generate a unique request ID (thread-safe)
|
|
286
|
+
# @return [Integer]
|
|
287
|
+
def generate_request_id
|
|
288
|
+
@@id_monitor.synchronize do
|
|
289
|
+
@@request_counter += 1
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|