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,224 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module LiveQuery
|
|
6
|
+
# Centralized configuration for LiveQuery client.
|
|
7
|
+
#
|
|
8
|
+
# @example Configure LiveQuery
|
|
9
|
+
# Parse::LiveQuery.configure do |config|
|
|
10
|
+
# config.url = "wss://your-server.com"
|
|
11
|
+
# config.ping_interval = 20.0
|
|
12
|
+
# config.logging_enabled = true
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
class Configuration
|
|
16
|
+
# Connection settings
|
|
17
|
+
# @return [String] WebSocket URL for LiveQuery server
|
|
18
|
+
attr_accessor :url
|
|
19
|
+
|
|
20
|
+
# @return [String] Parse application ID
|
|
21
|
+
attr_accessor :application_id
|
|
22
|
+
|
|
23
|
+
# @return [String] Parse client key
|
|
24
|
+
attr_accessor :client_key
|
|
25
|
+
|
|
26
|
+
# @return [String] Parse master key (optional)
|
|
27
|
+
attr_accessor :master_key
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] automatically connect on client creation (default: true)
|
|
30
|
+
attr_accessor :auto_connect
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] automatically reconnect on disconnect (default: true)
|
|
33
|
+
attr_accessor :auto_reconnect
|
|
34
|
+
|
|
35
|
+
# Health monitoring settings
|
|
36
|
+
# @return [Float] seconds between ping frames (default: 30.0)
|
|
37
|
+
attr_accessor :ping_interval
|
|
38
|
+
|
|
39
|
+
# @return [Float] seconds to wait for pong response (default: 10.0)
|
|
40
|
+
attr_accessor :pong_timeout
|
|
41
|
+
|
|
42
|
+
# Circuit breaker settings
|
|
43
|
+
# @return [Integer] failures before circuit opens (default: 5)
|
|
44
|
+
attr_accessor :circuit_failure_threshold
|
|
45
|
+
|
|
46
|
+
# @return [Float] seconds before circuit transitions to half-open (default: 60.0)
|
|
47
|
+
attr_accessor :circuit_reset_timeout
|
|
48
|
+
|
|
49
|
+
# Reconnection backoff settings
|
|
50
|
+
# @return [Float] initial reconnect delay in seconds (default: 1.0)
|
|
51
|
+
attr_accessor :initial_reconnect_interval
|
|
52
|
+
|
|
53
|
+
# @return [Float] maximum reconnect delay in seconds (default: 30.0)
|
|
54
|
+
attr_accessor :max_reconnect_interval
|
|
55
|
+
|
|
56
|
+
# @return [Float] reconnect delay multiplier (default: 1.5)
|
|
57
|
+
attr_accessor :reconnect_multiplier
|
|
58
|
+
|
|
59
|
+
# @return [Float] jitter factor for reconnect delay, 0.0-1.0 (default: 0.2)
|
|
60
|
+
attr_accessor :reconnect_jitter
|
|
61
|
+
|
|
62
|
+
# Event queue settings
|
|
63
|
+
# @return [Integer] maximum queued events before backpressure (default: 1000)
|
|
64
|
+
attr_accessor :event_queue_size
|
|
65
|
+
|
|
66
|
+
# @return [Symbol] backpressure strategy :block, :drop_oldest, :drop_newest (default: :drop_oldest)
|
|
67
|
+
attr_accessor :backpressure_strategy
|
|
68
|
+
|
|
69
|
+
# Security settings
|
|
70
|
+
# @return [Integer] maximum WebSocket message size in bytes (default: 1MB)
|
|
71
|
+
# Prevents memory exhaustion from malicious oversized frames
|
|
72
|
+
attr_accessor :max_message_size
|
|
73
|
+
|
|
74
|
+
# @return [Integer] frame read timeout in seconds (default: 30)
|
|
75
|
+
# Prevents indefinite blocking when reading from socket
|
|
76
|
+
attr_accessor :frame_read_timeout
|
|
77
|
+
|
|
78
|
+
# @return [Boolean] when false (default), refuse to derive a `ws://`
|
|
79
|
+
# URL from an `http://` server URL on any non-loopback host. The
|
|
80
|
+
# default `Parse::LiveQuery::Client#derive_websocket_url` path
|
|
81
|
+
# silently picks `ws://` when the Parse server URL is `http://`,
|
|
82
|
+
# carrying master keys and session tokens over a cleartext
|
|
83
|
+
# socket. Set to `true` to explicitly opt into insecure
|
|
84
|
+
# WebSocket transport (local development, container-internal
|
|
85
|
+
# networks). Loopback hosts (`localhost`, `127.0.0.1`, `::1`)
|
|
86
|
+
# are exempt and emit a warning instead.
|
|
87
|
+
attr_accessor :allow_insecure
|
|
88
|
+
|
|
89
|
+
# @return [Symbol, nil] minimum TLS version :TLSv1, :TLSv1_1, :TLSv1_2, :TLSv1_3 (default: :TLSv1_2)
|
|
90
|
+
# Enforces minimum TLS version for WebSocket connections
|
|
91
|
+
attr_accessor :ssl_min_version
|
|
92
|
+
|
|
93
|
+
# @return [Symbol, nil] maximum TLS version :TLSv1, :TLSv1_1, :TLSv1_2, :TLSv1_3 (default: nil = highest available)
|
|
94
|
+
# Caps the maximum TLS version (rarely needed, use for compatibility)
|
|
95
|
+
attr_accessor :ssl_max_version
|
|
96
|
+
|
|
97
|
+
# Map of TLS version symbols to OpenSSL constants
|
|
98
|
+
TLS_VERSION_MAP = {
|
|
99
|
+
TLSv1: OpenSSL::SSL::TLS1_VERSION,
|
|
100
|
+
TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION,
|
|
101
|
+
TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION,
|
|
102
|
+
TLSv1_3: OpenSSL::SSL::TLS1_3_VERSION,
|
|
103
|
+
}.freeze
|
|
104
|
+
|
|
105
|
+
# Valid TLS version symbols
|
|
106
|
+
VALID_TLS_VERSIONS = [nil, :TLSv1, :TLSv1_1, :TLSv1_2, :TLSv1_3].freeze
|
|
107
|
+
|
|
108
|
+
# Convert a TLS version symbol to OpenSSL constant
|
|
109
|
+
# @param version [Symbol, nil] TLS version symbol
|
|
110
|
+
# @return [Integer, nil] OpenSSL TLS version constant or nil
|
|
111
|
+
def self.tls_version_constant(version)
|
|
112
|
+
return nil if version.nil?
|
|
113
|
+
TLS_VERSION_MAP[version]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Logging settings
|
|
117
|
+
# @return [Boolean] enable structured logging (default: false)
|
|
118
|
+
attr_accessor :logging_enabled
|
|
119
|
+
|
|
120
|
+
# @return [Symbol] log level :debug, :info, :warn, :error (default: :info)
|
|
121
|
+
attr_accessor :log_level
|
|
122
|
+
|
|
123
|
+
# @return [Logger, nil] custom logger instance (default: nil, uses STDOUT)
|
|
124
|
+
attr_accessor :logger
|
|
125
|
+
|
|
126
|
+
# Initialize with sensible defaults
|
|
127
|
+
def initialize
|
|
128
|
+
# Connection
|
|
129
|
+
@url = nil
|
|
130
|
+
@application_id = nil
|
|
131
|
+
@client_key = nil
|
|
132
|
+
@master_key = nil
|
|
133
|
+
@auto_connect = true
|
|
134
|
+
@auto_reconnect = true
|
|
135
|
+
|
|
136
|
+
# Health monitoring
|
|
137
|
+
@ping_interval = 30.0
|
|
138
|
+
@pong_timeout = 10.0
|
|
139
|
+
|
|
140
|
+
# Circuit breaker
|
|
141
|
+
@circuit_failure_threshold = 5
|
|
142
|
+
@circuit_reset_timeout = 60.0
|
|
143
|
+
|
|
144
|
+
# Reconnection backoff
|
|
145
|
+
@initial_reconnect_interval = 1.0
|
|
146
|
+
@max_reconnect_interval = 30.0
|
|
147
|
+
@reconnect_multiplier = 1.5
|
|
148
|
+
@reconnect_jitter = 0.2
|
|
149
|
+
|
|
150
|
+
# Event queue
|
|
151
|
+
@event_queue_size = 1000
|
|
152
|
+
@backpressure_strategy = :drop_oldest
|
|
153
|
+
|
|
154
|
+
# Security
|
|
155
|
+
@max_message_size = 1_048_576 # 1MB
|
|
156
|
+
@frame_read_timeout = 30 # 30 seconds
|
|
157
|
+
@ssl_min_version = :TLSv1_2 # Enforce modern TLS by default
|
|
158
|
+
@ssl_max_version = nil # No maximum (use highest available)
|
|
159
|
+
@allow_insecure = false # Refuse ws:// downgrade on non-loopback hosts
|
|
160
|
+
|
|
161
|
+
# Logging
|
|
162
|
+
@logging_enabled = false
|
|
163
|
+
@log_level = :info
|
|
164
|
+
@logger = nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Validate configuration
|
|
168
|
+
# @return [Array<String>] list of validation errors
|
|
169
|
+
def validate
|
|
170
|
+
errors = []
|
|
171
|
+
errors << "ping_interval must be positive" if @ping_interval && @ping_interval <= 0
|
|
172
|
+
errors << "pong_timeout must be positive" if @pong_timeout && @pong_timeout <= 0
|
|
173
|
+
errors << "circuit_failure_threshold must be positive" if @circuit_failure_threshold && @circuit_failure_threshold <= 0
|
|
174
|
+
errors << "event_queue_size must be positive" if @event_queue_size && @event_queue_size <= 0
|
|
175
|
+
errors << "reconnect_jitter must be between 0.0 and 1.0" if @reconnect_jitter && (@reconnect_jitter < 0.0 || @reconnect_jitter > 1.0)
|
|
176
|
+
errors << "backpressure_strategy must be :block, :drop_oldest, or :drop_newest" unless [:block, :drop_oldest, :drop_newest].include?(@backpressure_strategy)
|
|
177
|
+
errors << "max_message_size must be positive" if @max_message_size && @max_message_size <= 0
|
|
178
|
+
errors << "frame_read_timeout must be positive" if @frame_read_timeout && @frame_read_timeout <= 0
|
|
179
|
+
errors << "log_level must be :debug, :info, :warn, or :error" unless [:debug, :info, :warn, :error].include?(@log_level)
|
|
180
|
+
|
|
181
|
+
# SSL/TLS version validation
|
|
182
|
+
errors << "ssl_min_version must be nil, :TLSv1, :TLSv1_1, :TLSv1_2, or :TLSv1_3" unless VALID_TLS_VERSIONS.include?(@ssl_min_version)
|
|
183
|
+
errors << "ssl_max_version must be nil, :TLSv1, :TLSv1_1, :TLSv1_2, or :TLSv1_3" unless VALID_TLS_VERSIONS.include?(@ssl_max_version)
|
|
184
|
+
|
|
185
|
+
errors
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check if configuration is valid
|
|
189
|
+
# @return [Boolean]
|
|
190
|
+
def valid?
|
|
191
|
+
validate.empty?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Convert to hash
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def to_h
|
|
197
|
+
{
|
|
198
|
+
url: @url,
|
|
199
|
+
application_id: @application_id,
|
|
200
|
+
client_key: @client_key.nil? ? nil : "[REDACTED]",
|
|
201
|
+
master_key: @master_key.nil? ? nil : "[REDACTED]",
|
|
202
|
+
auto_connect: @auto_connect,
|
|
203
|
+
auto_reconnect: @auto_reconnect,
|
|
204
|
+
ping_interval: @ping_interval,
|
|
205
|
+
pong_timeout: @pong_timeout,
|
|
206
|
+
circuit_failure_threshold: @circuit_failure_threshold,
|
|
207
|
+
circuit_reset_timeout: @circuit_reset_timeout,
|
|
208
|
+
initial_reconnect_interval: @initial_reconnect_interval,
|
|
209
|
+
max_reconnect_interval: @max_reconnect_interval,
|
|
210
|
+
reconnect_multiplier: @reconnect_multiplier,
|
|
211
|
+
reconnect_jitter: @reconnect_jitter,
|
|
212
|
+
event_queue_size: @event_queue_size,
|
|
213
|
+
backpressure_strategy: @backpressure_strategy,
|
|
214
|
+
max_message_size: @max_message_size,
|
|
215
|
+
frame_read_timeout: @frame_read_timeout,
|
|
216
|
+
ssl_min_version: @ssl_min_version,
|
|
217
|
+
ssl_max_version: @ssl_max_version,
|
|
218
|
+
logging_enabled: @logging_enabled,
|
|
219
|
+
log_level: @log_level,
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module LiveQuery
|
|
6
|
+
# Represents an event received from the LiveQuery server.
|
|
7
|
+
# Events are emitted when objects matching a subscription's query are
|
|
8
|
+
# created, updated, deleted, or enter/leave the query results.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# subscription.on(:update) do |event|
|
|
12
|
+
# puts "Object updated: #{event.object.id}"
|
|
13
|
+
# puts "Original state: #{event.original&.to_h}"
|
|
14
|
+
# puts "Event type: #{event.type}"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
class Event
|
|
18
|
+
# @return [Symbol] the type of event (:create, :update, :delete, :enter, :leave)
|
|
19
|
+
attr_reader :type
|
|
20
|
+
|
|
21
|
+
# @return [Parse::Object] the object affected by this event (current state)
|
|
22
|
+
attr_reader :object
|
|
23
|
+
|
|
24
|
+
# @return [Parse::Object, nil] the original state of the object (for :update, :enter, :leave)
|
|
25
|
+
attr_reader :original
|
|
26
|
+
|
|
27
|
+
# @return [Integer] the subscription request ID this event belongs to
|
|
28
|
+
attr_reader :request_id
|
|
29
|
+
|
|
30
|
+
# @return [String] the Parse class name
|
|
31
|
+
attr_reader :class_name
|
|
32
|
+
|
|
33
|
+
# @return [Time] when the event was received
|
|
34
|
+
attr_reader :received_at
|
|
35
|
+
|
|
36
|
+
# @return [Hash] raw payload from the server
|
|
37
|
+
attr_reader :raw
|
|
38
|
+
|
|
39
|
+
# Create a new Event from a LiveQuery server message
|
|
40
|
+
# @param type [Symbol] event type
|
|
41
|
+
# @param class_name [String] Parse class name
|
|
42
|
+
# @param object_data [Hash] object data from server
|
|
43
|
+
# @param original_data [Hash, nil] original object data (for update/enter/leave)
|
|
44
|
+
# @param request_id [Integer] subscription request ID
|
|
45
|
+
# @param raw [Hash] raw server payload
|
|
46
|
+
def initialize(type:, class_name:, object_data:, original_data: nil, request_id:, raw: {})
|
|
47
|
+
@type = type.to_sym
|
|
48
|
+
@class_name = class_name
|
|
49
|
+
@request_id = request_id
|
|
50
|
+
@received_at = Time.now
|
|
51
|
+
@raw = raw
|
|
52
|
+
|
|
53
|
+
# Convert object data to Parse::Object instances
|
|
54
|
+
@object = build_object(class_name, object_data) if object_data
|
|
55
|
+
@original = build_object(class_name, original_data) if original_data
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Boolean] true if this is a create event
|
|
59
|
+
def create?
|
|
60
|
+
type == :create
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Boolean] true if this is an update event
|
|
64
|
+
def update?
|
|
65
|
+
type == :update
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] true if this is a delete event
|
|
69
|
+
def delete?
|
|
70
|
+
type == :delete
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Boolean] true if this is an enter event (object now matches query)
|
|
74
|
+
def enter?
|
|
75
|
+
type == :enter
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Boolean] true if this is a leave event (object no longer matches query)
|
|
79
|
+
def leave?
|
|
80
|
+
type == :leave
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [String] the Parse object ID
|
|
84
|
+
def parse_object_id
|
|
85
|
+
object&.id
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Hash] event as a hash
|
|
89
|
+
def to_h
|
|
90
|
+
{
|
|
91
|
+
type: type,
|
|
92
|
+
class_name: class_name,
|
|
93
|
+
object_id: parse_object_id,
|
|
94
|
+
request_id: request_id,
|
|
95
|
+
received_at: received_at,
|
|
96
|
+
object: object&.as_json,
|
|
97
|
+
original: original&.as_json,
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Build a Parse::Object from hash data
|
|
104
|
+
# @param class_name [String] Parse class name
|
|
105
|
+
# @param data [Hash] object attributes
|
|
106
|
+
# @return [Parse::Object]
|
|
107
|
+
def build_object(class_name, data)
|
|
108
|
+
return nil unless data.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
# Use Parse::Object.build which handles class lookup and data application
|
|
111
|
+
Parse::Object.build(data, class_name)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "monitor"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module LiveQuery
|
|
8
|
+
# Error raised when event queue is full and strategy is :error
|
|
9
|
+
class EventQueueFullError < Error
|
|
10
|
+
def initialize(max_size)
|
|
11
|
+
super("Event queue full (max: #{max_size})")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Bounded event queue with configurable backpressure strategies.
|
|
16
|
+
#
|
|
17
|
+
# Provides a buffer between the WebSocket reader thread and callback
|
|
18
|
+
# execution, preventing high-frequency events from overwhelming the system.
|
|
19
|
+
#
|
|
20
|
+
# Backpressure Strategies:
|
|
21
|
+
# - :block - Block enqueue until space available (can cause reader thread to block)
|
|
22
|
+
# - :drop_oldest - Drop oldest events when full (default)
|
|
23
|
+
# - :drop_newest - Drop incoming events when full
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# queue = EventQueue.new(max_size: 1000, strategy: :drop_oldest)
|
|
27
|
+
# queue.start { |event| process_event(event) }
|
|
28
|
+
# queue.enqueue(event)
|
|
29
|
+
# queue.stop(drain: true)
|
|
30
|
+
#
|
|
31
|
+
class EventQueue
|
|
32
|
+
# Valid backpressure strategies
|
|
33
|
+
STRATEGIES = [:block, :drop_oldest, :drop_newest].freeze
|
|
34
|
+
|
|
35
|
+
# Default maximum queue size
|
|
36
|
+
DEFAULT_MAX_SIZE = 1000
|
|
37
|
+
|
|
38
|
+
# Default backpressure strategy
|
|
39
|
+
DEFAULT_STRATEGY = :drop_oldest
|
|
40
|
+
|
|
41
|
+
# @return [Integer] maximum queue size
|
|
42
|
+
attr_reader :max_size
|
|
43
|
+
|
|
44
|
+
# @return [Symbol] backpressure strategy
|
|
45
|
+
attr_reader :strategy
|
|
46
|
+
|
|
47
|
+
# @return [Integer] number of dropped events
|
|
48
|
+
attr_reader :dropped_count
|
|
49
|
+
|
|
50
|
+
# @return [Integer] total events enqueued
|
|
51
|
+
attr_reader :enqueued_count
|
|
52
|
+
|
|
53
|
+
# @return [Integer] total events processed
|
|
54
|
+
attr_reader :processed_count
|
|
55
|
+
|
|
56
|
+
# Create a new event queue
|
|
57
|
+
# @param max_size [Integer] maximum queue size
|
|
58
|
+
# @param strategy [Symbol] backpressure strategy (:block, :drop_oldest, :drop_newest)
|
|
59
|
+
# @param on_drop [Proc, nil] callback when events are dropped (receives event, reason)
|
|
60
|
+
def initialize(max_size: DEFAULT_MAX_SIZE, strategy: DEFAULT_STRATEGY, on_drop: nil)
|
|
61
|
+
unless STRATEGIES.include?(strategy)
|
|
62
|
+
raise ArgumentError, "Invalid strategy: #{strategy}. Must be one of #{STRATEGIES.inspect}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@max_size = max_size
|
|
66
|
+
@strategy = strategy
|
|
67
|
+
@on_drop = on_drop
|
|
68
|
+
|
|
69
|
+
@queue = []
|
|
70
|
+
@monitor = Monitor.new
|
|
71
|
+
@condition = @monitor.new_cond
|
|
72
|
+
@running = false
|
|
73
|
+
@processor_thread = nil
|
|
74
|
+
|
|
75
|
+
@dropped_count = 0
|
|
76
|
+
@enqueued_count = 0
|
|
77
|
+
@processed_count = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Start the event processor thread
|
|
81
|
+
# @yield [event] Block to process each event
|
|
82
|
+
# @return [void]
|
|
83
|
+
def start(&processor)
|
|
84
|
+
raise ArgumentError, "Processor block required" unless block_given?
|
|
85
|
+
|
|
86
|
+
@monitor.synchronize do
|
|
87
|
+
return if @running
|
|
88
|
+
|
|
89
|
+
@running = true
|
|
90
|
+
@processor_thread = Thread.new { process_loop(&processor) }
|
|
91
|
+
@processor_thread.abort_on_exception = false
|
|
92
|
+
|
|
93
|
+
Logging.debug("Event queue started", max_size: @max_size, strategy: @strategy)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Stop the event processor
|
|
98
|
+
# @param drain [Boolean] process remaining events before stopping
|
|
99
|
+
# @param timeout [Float] seconds to wait for drain
|
|
100
|
+
# @return [void]
|
|
101
|
+
def stop(drain: true, timeout: 5.0)
|
|
102
|
+
@monitor.synchronize do
|
|
103
|
+
return unless @running
|
|
104
|
+
|
|
105
|
+
@running = false
|
|
106
|
+
@condition.broadcast
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if drain && @processor_thread
|
|
110
|
+
@processor_thread.join(timeout)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@processor_thread&.kill
|
|
114
|
+
@processor_thread = nil
|
|
115
|
+
|
|
116
|
+
remaining = @monitor.synchronize { @queue.size }
|
|
117
|
+
Logging.debug("Event queue stopped", remaining: remaining, dropped: @dropped_count)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Add an event to the queue
|
|
121
|
+
# @param event [Object] the event to enqueue
|
|
122
|
+
# @return [Boolean] true if enqueued, false if dropped
|
|
123
|
+
def enqueue(event)
|
|
124
|
+
@monitor.synchronize do
|
|
125
|
+
return false unless @running
|
|
126
|
+
|
|
127
|
+
if @queue.size >= @max_size
|
|
128
|
+
handle_backpressure(event)
|
|
129
|
+
else
|
|
130
|
+
@queue << event
|
|
131
|
+
@enqueued_count += 1
|
|
132
|
+
@condition.signal
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Current queue size
|
|
139
|
+
# @return [Integer]
|
|
140
|
+
def size
|
|
141
|
+
@monitor.synchronize { @queue.size }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check if queue is full
|
|
145
|
+
# @return [Boolean]
|
|
146
|
+
def full?
|
|
147
|
+
@monitor.synchronize { @queue.size >= @max_size }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check if queue is empty
|
|
151
|
+
# @return [Boolean]
|
|
152
|
+
def empty?
|
|
153
|
+
@monitor.synchronize { @queue.empty? }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check if queue is running
|
|
157
|
+
# @return [Boolean]
|
|
158
|
+
def running?
|
|
159
|
+
@monitor.synchronize { @running }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get queue statistics
|
|
163
|
+
# @return [Hash]
|
|
164
|
+
def stats
|
|
165
|
+
@monitor.synchronize do
|
|
166
|
+
{
|
|
167
|
+
size: @queue.size,
|
|
168
|
+
max_size: @max_size,
|
|
169
|
+
strategy: @strategy,
|
|
170
|
+
running: @running,
|
|
171
|
+
enqueued_count: @enqueued_count,
|
|
172
|
+
processed_count: @processed_count,
|
|
173
|
+
dropped_count: @dropped_count,
|
|
174
|
+
utilization: @max_size > 0 ? (@queue.size.to_f / @max_size * 100).round(1) : 0,
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Clear the queue
|
|
180
|
+
# @return [Integer] number of events cleared
|
|
181
|
+
def clear
|
|
182
|
+
@monitor.synchronize do
|
|
183
|
+
count = @queue.size
|
|
184
|
+
@queue.clear
|
|
185
|
+
count
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
# Main processing loop - runs in background thread
|
|
192
|
+
def process_loop
|
|
193
|
+
while @running
|
|
194
|
+
event = nil
|
|
195
|
+
|
|
196
|
+
@monitor.synchronize do
|
|
197
|
+
# Wait for events or stop signal
|
|
198
|
+
while @queue.empty? && @running
|
|
199
|
+
@condition.wait(1.0)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
event = @queue.shift if @running || !@queue.empty?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if event
|
|
206
|
+
begin
|
|
207
|
+
yield event
|
|
208
|
+
@monitor.synchronize { @processed_count += 1 }
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
Logging.error("Event processing error", error: e)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Drain remaining events if requested
|
|
216
|
+
drain_remaining { |e| yield e }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Drain remaining events after stop
|
|
220
|
+
def drain_remaining
|
|
221
|
+
loop do
|
|
222
|
+
event = @monitor.synchronize { @queue.shift }
|
|
223
|
+
break unless event
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
yield event
|
|
227
|
+
@monitor.synchronize { @processed_count += 1 }
|
|
228
|
+
rescue StandardError => e
|
|
229
|
+
Logging.error("Event drain error", error: e)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Handle backpressure when queue is full
|
|
235
|
+
# @param event [Object] the event being enqueued
|
|
236
|
+
# @return [Boolean] true if enqueued, false if dropped
|
|
237
|
+
def handle_backpressure(event)
|
|
238
|
+
case @strategy
|
|
239
|
+
when :block
|
|
240
|
+
# Wait until space available
|
|
241
|
+
@condition.wait until @queue.size < @max_size || !@running
|
|
242
|
+
if @running
|
|
243
|
+
@queue << event
|
|
244
|
+
@enqueued_count += 1
|
|
245
|
+
true
|
|
246
|
+
else
|
|
247
|
+
false
|
|
248
|
+
end
|
|
249
|
+
when :drop_oldest
|
|
250
|
+
dropped = @queue.shift
|
|
251
|
+
@dropped_count += 1
|
|
252
|
+
notify_drop(dropped, :oldest)
|
|
253
|
+
@queue << event
|
|
254
|
+
@enqueued_count += 1
|
|
255
|
+
true
|
|
256
|
+
when :drop_newest
|
|
257
|
+
@dropped_count += 1
|
|
258
|
+
notify_drop(event, :newest)
|
|
259
|
+
false
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Notify callback of dropped event
|
|
264
|
+
def notify_drop(event, reason)
|
|
265
|
+
Logging.warn("Event dropped", reason: reason, queue_size: @queue.size)
|
|
266
|
+
@on_drop&.call(event, reason)
|
|
267
|
+
rescue StandardError => e
|
|
268
|
+
Logging.error("Drop callback error", error: e)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|