Class: Parse::LiveQuery::Subscription

Inherits:
Object
  • Object
show all
Defined in:
lib/parse/live_query/subscription.rb

Overview

Represents an active subscription to a LiveQuery. Manages event callbacks and subscription lifecycle.

Examples:

subscription = Song.subscribe(where: { artist: "Beatles" })

# Register callbacks using on() method
subscription.on(:create) { |song| puts "New song!" }
subscription.on(:update) { |song, original| puts "Updated!" }

# Or use shorthand methods
subscription.on_create { |song| puts "New song!" }
subscription.on_update { |song, original| puts "Updated!" }
subscription.on_delete { |song| puts "Deleted!" }
subscription.on_enter { |song, original| puts "Entered query!" }
subscription.on_leave { |song, original| puts "Left query!" }

# Error handling
subscription.on_error { |error| puts "Error: #{error.message}" }

# Connection events
subscription.on_subscribe { puts "Subscribed!" }
subscription.on_unsubscribe { puts "Unsubscribed!" }

# Cleanup
subscription.unsubscribe

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client:, class_name:, query: {}, fields: nil, session_token: nil, use_master_key: false) ⇒ Subscription

Create a new subscription

Parameters:

  • client (Parse::LiveQuery::Client)

    the LiveQuery client

  • class_name (String)

    Parse class name

  • query (Hash) (defaults to: {})

    query constraints (where clause)

  • fields (Array<String>, nil) (defaults to: nil)

    specific fields to watch

  • session_token (String, nil) (defaults to: nil)

    session token for authentication

  • use_master_key (Boolean) (defaults to: false)

    an intent assertion that this subscription needs master-key (ACL-bypassing) scope. It does NOT put masterKey on the subscribe frame: Parse Server has no per-subscription master key — client.hasMasterKey is fixed per connection at connect time, so one subscription on a scoped socket can never be selectively elevated. The flag is honored only when the parent client is an admin connection (built with use_master_key: true), where the whole connection is already elevated; on a non-admin connection the client warns and the subscription stays ACL-scoped. For mixed scoped + admin needs, use two separate clients. Defaults to false.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/parse/live_query/subscription.rb', line 88

def initialize(client:, class_name:, query: {}, fields: nil,
               session_token: nil, use_master_key: false)
  @monitor = Monitor.new
  @client = client
  @class_name = class_name
  @query = query
  @fields = fields
  @session_token = session_token
  @use_master_key = use_master_key == true
  @request_id = generate_request_id
  @state = :pending
  @callbacks = Hash.new { |h, k| h[k] = [] }

  Logging.debug("Subscription created",
                request_id: @request_id,
                class_name: @class_name,
                query_keys: @query.keys)
end

Instance Attribute Details

#class_nameString (readonly)

Returns Parse class name being subscribed to.

Returns:

  • (String)

    Parse class name being subscribed to



57
58
59
# File 'lib/parse/live_query/subscription.rb', line 57

def class_name
  @class_name
end

#clientParse::LiveQuery::Client (readonly)

Returns the LiveQuery client.

Returns:



63
64
65
# File 'lib/parse/live_query/subscription.rb', line 63

def client
  @client
end

#fieldsArray<String> (readonly)

Returns fields to watch for changes (nil = all fields).

Returns:

  • (Array<String>)

    fields to watch for changes (nil = all fields)



66
67
68
# File 'lib/parse/live_query/subscription.rb', line 66

def fields
  @fields
end

#queryHash (readonly)

Returns the query constraints (where clause).

Returns:

  • (Hash)

    the query constraints (where clause)



60
61
62
# File 'lib/parse/live_query/subscription.rb', line 60

def query
  @query
end

#request_idInteger (readonly)

Returns unique request ID for this subscription.

Returns:

  • (Integer)

    unique request ID for this subscription



54
55
56
# File 'lib/parse/live_query/subscription.rb', line 54

def request_id
  @request_id
end

#session_tokenString? (readonly)

Returns session token for ACL-aware subscriptions.

Returns:

  • (String, nil)

    session token for ACL-aware subscriptions



69
70
71
# File 'lib/parse/live_query/subscription.rb', line 69

def session_token
  @session_token
end

Instance Method Details

#confirm!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Mark subscription as confirmed by server



285
286
287
288
289
290
291
# File 'lib/parse/live_query/subscription.rb', line 285

def confirm!
  @monitor.synchronize { @state = :subscribed }
  Logging.info("Subscription confirmed",
               request_id: @request_id,
               class_name: @class_name)
  emit(:subscribe)
end

#error?Boolean

Returns true if in error state.

Returns:

  • (Boolean)

    true if in error state



226
227
228
# File 'lib/parse/live_query/subscription.rb', line 226

def error?
  state == :error
end

#fail!(error) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Mark subscription as failed with error

Parameters:



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/parse/live_query/subscription.rb', line 296

def fail!(error)
  @monitor.synchronize { @state = :error }
  # Promote String errors (which come back from the LiveQuery
  # server with messages like "Permission denied (code: 101)")
  # to typed SubscriptionError instances carrying the request_id
  # and class_name as structured context. The resulting
  # `e.message` reads `request_id=<n> class=<X> <server message>`,
  # so a single-line log captures the operational context the
  # raw server string lacks.
  if error.is_a?(String)
    error = SubscriptionError.new(error, request_id: @request_id, class_name: @class_name)
  end
  Logging.error("Subscription failed",
                request_id: @request_id,
                class_name: @class_name,
                error: error)
  emit(:error, error)
end

#handle_event(event) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Handle an incoming event from the server

Parameters:



276
277
278
279
280
281
# File 'lib/parse/live_query/subscription.rb', line 276

def handle_event(event)
  Logging.debug("Handling event",
                request_id: @request_id,
                event_type: event.type)
  emit(event.type, event.object, event.original)
end

#inspectString

Redacting inspect — the default inspect would expose @session_token (and, via @client, the client's master/REST keys) in any log line, backtrace, error page, or error reporter that renders the subscription. Reads @state directly rather than through the monitor so a diagnostic inspect never blocks on the lock.

Returns:



120
121
122
123
124
125
# File 'lib/parse/live_query/subscription.rb', line 120

def inspect
  token = @session_token.nil? || @session_token.empty? ? "nil" : "[REDACTED]"
  "#<#{self.class.name} request_id=#{@request_id.inspect} " \
  "class_name=#{@class_name.inspect} state=#{@state.inspect} " \
  "use_master_key=#{@use_master_key} session_token=#{token}>"
end

#on(event_type) {|object, original| ... } ⇒ self

Register a callback for a specific event type

Parameters:

  • event_type (Symbol)

    :create, :update, :delete, :enter, :leave, :error, :subscribe, :unsubscribe

Yields:

  • (object, original)

    block to call when event occurs

Returns:

  • (self)


131
132
133
134
135
136
137
138
# File 'lib/parse/live_query/subscription.rb', line 131

def on(event_type, &block)
  return self unless block_given?

  @monitor.synchronize do
    @callbacks[event_type.to_sym] << block
  end
  self
end

#on_create {|Parse::Object| ... } ⇒ self

Register callback for create events

Yields:

Returns:

  • (self)


143
144
145
# File 'lib/parse/live_query/subscription.rb', line 143

def on_create(&block)
  on(:create, &block)
end

#on_delete {|Parse::Object| ... } ⇒ self

Register callback for delete events

Yields:

Returns:

  • (self)


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

def on_delete(&block)
  on(:delete, &block)
end

#on_enter {|Parse::Object, Parse::Object| ... } ⇒ self

Register callback for enter events (object now matches query)

Yields:

Returns:

  • (self)


164
165
166
# File 'lib/parse/live_query/subscription.rb', line 164

def on_enter(&block)
  on(:enter, &block)
end

#on_error {|Exception| ... } ⇒ self

Register callback for errors

Yields:

  • (Exception)

    the error that occurred

Returns:

  • (self)


178
179
180
# File 'lib/parse/live_query/subscription.rb', line 178

def on_error(&block)
  on(:error, &block)
end

#on_leave {|Parse::Object, Parse::Object| ... } ⇒ self

Register callback for leave events (object no longer matches query)

Yields:

Returns:

  • (self)


171
172
173
# File 'lib/parse/live_query/subscription.rb', line 171

def on_leave(&block)
  on(:leave, &block)
end

#on_subscribe { ... } ⇒ self

Register callback for successful subscription

Yields:

  • called when subscription is confirmed

Returns:

  • (self)


185
186
187
# File 'lib/parse/live_query/subscription.rb', line 185

def on_subscribe(&block)
  on(:subscribe, &block)
end

#on_unsubscribe { ... } ⇒ self

Register callback for unsubscription

Yields:

  • called when unsubscribed

Returns:

  • (self)


192
193
194
# File 'lib/parse/live_query/subscription.rb', line 192

def on_unsubscribe(&block)
  on(:unsubscribe, &block)
end

#on_update {|Parse::Object, Parse::Object| ... } ⇒ self

Register callback for update events

Yields:

Returns:

  • (self)


150
151
152
# File 'lib/parse/live_query/subscription.rb', line 150

def on_update(&block)
  on(:update, &block)
end

#pending?Boolean

Returns true if pending subscription confirmation.

Returns:

  • (Boolean)

    true if pending subscription confirmation



216
217
218
# File 'lib/parse/live_query/subscription.rb', line 216

def pending?
  state == :pending
end

#stateSymbol

Current subscription state

Returns:

  • (Symbol)

    :pending, :subscribed, :unsubscribed, or :error



109
110
111
# File 'lib/parse/live_query/subscription.rb', line 109

def state
  @monitor.synchronize { @state }
end

#subscribed?Boolean

Returns true if currently subscribed.

Returns:

  • (Boolean)

    true if currently subscribed



211
212
213
# File 'lib/parse/live_query/subscription.rb', line 211

def subscribed?
  state == :subscribed
end

#to_hHash

Returns subscription info as hash.

Returns:

  • (Hash)

    subscription info as hash



316
317
318
319
320
321
322
323
324
325
326
# File 'lib/parse/live_query/subscription.rb', line 316

def to_h
  @monitor.synchronize do
    {
      request_id: request_id,
      class_name: class_name,
      query: query,
      state: @state,
      fields: fields,
    }
  end
end

#to_subscribe_messageHash

Build the subscription message to send to the server

Returns:



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/parse/live_query/subscription.rb', line 232

def to_subscribe_message
  msg = {
    op: "subscribe",
    requestId: request_id,
    query: {
      className: class_name,
      where: query,
    },
  }

  msg[:query][:fields] = fields if fields&.any?
  msg[:sessionToken] = session_token if session_token
  # The subscribe frame deliberately NEVER carries `masterKey`.
  # Parse Server's `_handleSubscribe` does not read it — master-key
  # (ACL-bypass) authorization is resolved once, per connection, in
  # `_handleConnect` (`client.hasMasterKey`). Emitting it here put a
  # privileged credential on the wire for ZERO server-side effect.
  # `use_master_key: true` at the subscription level is an intent
  # assertion validated by the client (which warns when it cannot
  # be honored on a non-admin connection); the actual elevation is
  # the admin connection's connect frame. See
  # {Parse::LiveQuery::Client#use_master_key}.

  msg
end

#to_unsubscribe_messageHash

Build the unsubscribe message

Returns:



266
267
268
269
270
271
# File 'lib/parse/live_query/subscription.rb', line 266

def to_unsubscribe_message
  {
    op: "unsubscribe",
    requestId: request_id,
  }
end

#unsubscribeBoolean

Unsubscribe from this subscription

Returns:

  • (Boolean)

    true if unsubscribe message was sent



198
199
200
201
202
203
204
205
206
207
208
# File 'lib/parse/live_query/subscription.rb', line 198

def unsubscribe
  @monitor.synchronize do
    return false if @state == :unsubscribed
    @state = :unsubscribed
  end

  Logging.debug("Unsubscribing", request_id: @request_id)
  client.unsubscribe(self)
  emit(:unsubscribe)
  true
end

#unsubscribed?Boolean

Returns true if unsubscribed.

Returns:

  • (Boolean)

    true if unsubscribed



221
222
223
# File 'lib/parse/live_query/subscription.rb', line 221

def unsubscribed?
  state == :unsubscribed
end

#use_master_key?Boolean

Returns whether this subscription opted into per-subscription master-key auth via use_master_key: true.

Returns:

  • (Boolean)

    whether this subscription opted into per-subscription master-key auth via use_master_key: true.



260
261
262
# File 'lib/parse/live_query/subscription.rb', line 260

def use_master_key?
  @use_master_key == true
end