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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. 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