Module: Parse::LiveQuery

Defined in:
lib/parse/live_query.rb,
lib/parse/live_query.rb,
lib/parse/live_query/event.rb,
lib/parse/live_query/client.rb,
lib/parse/live_query/logging.rb,
lib/parse/live_query/event_queue.rb,
lib/parse/live_query/subscription.rb,
lib/parse/live_query/configuration.rb,
lib/parse/live_query/health_monitor.rb,
lib/parse/live_query/circuit_breaker.rb

Overview

Note:

LiveQuery requires an explicit opt-in before any subscription will open a network connection. This is a safety gate (operator must consciously enable the WebSocket egress surface), not a stability warning. Set the toggle once at boot:

Parse.live_query_enabled = true

LiveQuery provides real-time data subscriptions for reactive applications. It uses WebSockets to receive push notifications when data changes on the server. Stable since Parse Stack 3.0.0.

Examples:

Basic usage

# Configure LiveQuery server URL
Parse.setup(
  application_id: "your_app_id",
  api_key: "your_api_key",
  server_url: "https://your-parse-server.com/parse",
  live_query_url: "wss://your-parse-server.com"
)

# Subscribe to changes on a model
subscription = Song.subscribe(where: { artist: "Artist Name" })

subscription.on(:create) { |song| puts "New song: #{song.title}" }
subscription.on(:update) { |song, original| puts "Updated: #{song.title}" }
subscription.on(:delete) { |song| puts "Deleted: #{song.id}" }
subscription.on(:enter) { |song, original| puts "Entered query: #{song.title}" }
subscription.on(:leave) { |song, original| puts "Left query: #{song.title}" }

# Unsubscribe when done
subscription.unsubscribe

Using Query directly

query = Song.query(:plays.gt => 1000)
subscription = query.subscribe

subscription.on_create { |song| puts "New popular song!" }
subscription.on_update { |song| puts "Song updated!" }

Multiple subscriptions

client = Parse::LiveQuery.client

sub1 = client.subscribe(Song, where: { genre: "rock" })
sub2 = client.subscribe(Album, where: { year: 2024 })

# Close all subscriptions
client.close

Defined Under Namespace

Modules: Logging Classes: AuthenticationError, CircuitBreaker, Client, Configuration, ConnectionError, Error, Event, EventQueue, EventQueueFullError, HealthMonitor, NotEnabledError, Subscription, SubscriptionError

Constant Summary collapse

EVENTS =

Default LiveQuery events

%i[create update delete enter leave].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.default_clientParse::LiveQuery::Client

Returns the default LiveQuery client.

Returns:



121
122
123
# File 'lib/parse/live_query.rb', line 121

def default_client
  @default_client
end

Class Method Details

.available?Boolean

Check if LiveQuery is configured and available

Returns:

  • (Boolean)


152
153
154
# File 'lib/parse/live_query.rb', line 152

def available?
  !!config.url
end

.clientParse::LiveQuery::Client

Get or create the default LiveQuery client. Uses the configuration from Parse.setup if available.

Returns:

Raises:



139
140
141
142
# File 'lib/parse/live_query.rb', line 139

def client
  ensure_enabled!
  @default_client ||= Client.new
end

.configParse::LiveQuery::Configuration

Get the LiveQuery configuration object



158
159
160
# File 'lib/parse/live_query.rb', line 158

def config
  @config ||= Configuration.new
end

.configurationHash

Deprecated.

Use configure block instead

Legacy configuration method for backward compatibility

Returns:



188
189
190
191
192
193
194
195
# File 'lib/parse/live_query.rb', line 188

def configuration
  {
    url: config.url,
    application_id: config.application_id,
    client_key: config.client_key,
    master_key: config.master_key,
  }
end

.configure {|config| ... } ⇒ Configuration

Configure LiveQuery settings using a block

Examples:

Parse::LiveQuery.configure do |config|
  config.url = "wss://your-server.com"
  config.ping_interval = 20.0
  config.logging_enabled = true
end

Yields:

  • (config)

    Configuration object

Returns:



172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/parse/live_query.rb', line 172

def configure
  yield config if block_given?

  # Sync logging settings
  if config.logging_enabled
    Logging.enabled = true
    Logging.log_level = config.log_level
    Logging.logger = config.logger if config.logger
  end

  config
end

.enabled?Boolean

Check if LiveQuery feature is enabled

Returns:

  • (Boolean)


125
126
127
# File 'lib/parse/live_query.rb', line 125

def enabled?
  Parse.live_query_enabled?
end

.ensure_enabled!Object

Ensure LiveQuery is enabled, raising an error if not

Raises:



131
132
133
# File 'lib/parse/live_query.rb', line 131

def ensure_enabled!
  raise NotEnabledError unless enabled?
end

.reset!Object

Reset the default client (closes connection and clears instance)



145
146
147
148
# File 'lib/parse/live_query.rb', line 145

def reset!
  @default_client&.close
  @default_client = nil
end

.run_until_signal!(client: nil, signals: %i[INT TERM],, shutdown_timeout: 5.0, poll_interval: 0.25) {|client| ... }

This method returns an undefined value.

Block until the process receives one of signals (default INT and TERM), then gracefully shut down the supplied LiveQuery client and return. Designed for long-running rake-task-style consumers of LiveQuery (rake livequery:tail, rake installations:watch, etc.) where the caller's natural idiom is "subscribe, then wait forever; on Ctrl-C, clean up."

Why a helper: Signal.trap blocks on MRI / macOS run in a restricted context — calling client.unsubscribe / client.close (which themselves take the client's internal Monitor) directly from the trap raises ThreadError: can't be called from trap context on the platforms that enforce :signal_safe?. The safe idiom is "set a flag in the trap, poll from the main thread, perform the shutdown there." This method bundles that idiom so callers don't have to re-derive it (and so they don't deploy the unsafe version and hit the ThreadError in production).

The supplied block (if any) runs once before the wait loop, so callers can hand-off subscription setup that should not race the trap installation:

Examples:

Parse::LiveQuery.run_until_signal! do |client|
  client.subscribe(Post, where: { published: true }) do |sub|
    sub.on(:create) { |obj| puts "new post: #{obj.id}" }
  end
end

Parameters:

  • client (Parse::LiveQuery::Client, nil) (defaults to: nil)

    client to shut down on signal. Defaults to Parse::LiveQuery.client (the process-wide default).

  • signals (Array<Symbol, String>) (defaults to: %i[INT TERM],)

    signal names to trap. Defaults to %i[INT TERM] — common for SIGINT (Ctrl-C) and SIGTERM (orchestrator stop).

  • shutdown_timeout (Float) (defaults to: 5.0)

    seconds to allow Parse::LiveQuery::Client#shutdown to drain pending events.

  • poll_interval (Float) (defaults to: 0.25)

    seconds between sentinel checks. Lower values reduce shutdown latency; higher values reduce wakeup overhead on idle processes. Default 0.25s.

Yield Parameters:



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/parse/live_query.rb', line 240

def run_until_signal!(client: nil, signals: %i[INT TERM],
                      shutdown_timeout: 5.0, poll_interval: 0.25)
  ensure_enabled!
  unless signals.is_a?(Array) && !signals.empty?
    raise ArgumentError,
          "Parse::LiveQuery.run_until_signal!: signals must be a non-empty Array " \
          "(got #{signals.inspect}). An empty list would block the poll loop forever " \
          "with no trap installed."
  end
  target = client || self.client

  # Sentinel is a single-element queue rather than an instance
  # variable so the trap handler does only the absolute minimum
  # work (one `push`) — no mutex acquisition, no allocation
  # beyond what `<<` does internally.
  stop_signal = Queue.new
  installed = []
  begin
    # Yield BEFORE installing traps (so a SIGINT during caller
    # setup still aborts normally) but INSIDE the begin/ensure so a
    # raise from the block — including Interrupt — still runs the
    # shutdown/restore cleanup below rather than leaking the
    # client's connection and threads.
    yield(target) if block_given?

    signals.each do |sig|
      prior = Signal.trap(sig) { stop_signal << sig }
      installed << [sig, prior]
    end

    # Block until a signal arrives. Use `Queue#pop` with the
    # poll loop so trap-context limitations don't matter — we
    # only ever ENQUEUE from the trap; the dequeue is here on
    # the main thread.
    loop do
      sig = stop_signal.pop(true) rescue nil
      break if sig
      sleep poll_interval
    end
  ensure
    # Restore the prior trap handlers so re-running the helper
    # (e.g. in tests, or in a parent process that traps INT
    # itself) does not leak our handler.
    installed.each { |sig, prior| Signal.trap(sig, prior) if prior }
    # Shutdown from the main thread, not the trap context.
    target.shutdown(timeout: shutdown_timeout) if target.respond_to?(:shutdown)
  end
end