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, keys: nil, session_token: nil, use_master_key: false, watch: nil) ⇒ 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.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/parse/live_query/subscription.rb', line 98

def initialize(client:, class_name:, query: {}, fields: nil, keys: nil,
               session_token: nil, use_master_key: false, watch: nil)
  @monitor = Monitor.new
  @client = client
  @class_name = class_name
  @query = query
  # `keys` is the post-7.0 name; accept either and prefer the explicit
  # `keys:` when both are supplied.
  @fields = keys.nil? ? fields : keys
  @watch = watch
  @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) Also known as: keys

Returns field projection for returned events (nil = all fields). Parse Server 7.0 renamed this subscription option from fields to keys; #keys is the canonical alias.

Returns:

  • (Array<String>)

    field projection for returned events (nil = all fields). Parse Server 7.0 renamed this subscription option from fields to keys; #keys is the canonical alias.



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

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



74
75
76
# File 'lib/parse/live_query/subscription.rb', line 74

def session_token
  @session_token
end

#watchArray<String>? (readonly)

Returns field names that trigger update events when changed (PS 7.0+ watch option). +nil+ means all field changes trigger update events.

Returns:

  • (Array<String>, nil)

    field names that trigger update events when changed (PS 7.0+ watch option). +nil+ means all field changes trigger update events.



79
80
81
# File 'lib/parse/live_query/subscription.rb', line 79

def watch
  @watch
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



312
313
314
315
316
317
318
# File 'lib/parse/live_query/subscription.rb', line 312

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



239
240
241
# File 'lib/parse/live_query/subscription.rb', line 239

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:



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/parse/live_query/subscription.rb', line 323

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:



303
304
305
306
307
308
# File 'lib/parse/live_query/subscription.rb', line 303

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:



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

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)


144
145
146
147
148
149
150
151
# File 'lib/parse/live_query/subscription.rb', line 144

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)


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

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

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

Register callback for delete events

Yields:

Returns:

  • (self)


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

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)


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

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

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

Register callback for errors

Yields:

  • (Exception)

    the error that occurred

Returns:

  • (self)


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

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)


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

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

#on_subscribe { ... } ⇒ self

Register callback for successful subscription

Yields:

  • called when subscription is confirmed

Returns:

  • (self)


198
199
200
# File 'lib/parse/live_query/subscription.rb', line 198

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

#on_unsubscribe { ... } ⇒ self

Register callback for unsubscription

Yields:

  • called when unsubscribed

Returns:

  • (self)


205
206
207
# File 'lib/parse/live_query/subscription.rb', line 205

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

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

Register callback for update events

Yields:

Returns:

  • (self)


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

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

#pending?Boolean

Returns true if pending subscription confirmation.

Returns:

  • (Boolean)

    true if pending subscription confirmation



229
230
231
# File 'lib/parse/live_query/subscription.rb', line 229

def pending?
  state == :pending
end

#stateSymbol

Current subscription state

Returns:

  • (Symbol)

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



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

def state
  @monitor.synchronize { @state }
end

#subscribed?Boolean

Returns true if currently subscribed.

Returns:

  • (Boolean)

    true if currently subscribed



224
225
226
# File 'lib/parse/live_query/subscription.rb', line 224

def subscribed?
  state == :subscribed
end

#to_hHash

Returns subscription info as hash.

Returns:

  • (Hash)

    subscription info as hash



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/parse/live_query/subscription.rb', line 343

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:



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
# File 'lib/parse/live_query/subscription.rb', line 245

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

  if fields&.any?
    # Parse Server 7.0 (DEPPS9 / #8852) renamed the subscription field-
    # projection option from `fields` to `keys`. PS 7+ reads `keys` and
    # ignores `fields`; PS < 7 reads `fields`. Emit BOTH so projection is
    # honored on every supported server — sending an extra key the server
    # ignores is harmless, while sending only `fields` silently disables
    # projection (events return all columns) on PS 7+.
    msg[:query][:keys] = fields
    msg[:query][:fields] = fields
  end
  # PS 7.0 (#8028) `watch`: fire update events only when the named fields
  # change. Distinct from field projection (`keys`/`fields`): `watch`
  # controls which field mutations generate an update event; `keys` controls
  # which fields are returned in the event payload.
  msg[:query][:watch] = watch if watch&.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:



293
294
295
296
297
298
# File 'lib/parse/live_query/subscription.rb', line 293

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

#unsubscribeBoolean

Unsubscribe from this subscription

Returns:

  • (Boolean)

    true if unsubscribe message was sent



211
212
213
214
215
216
217
218
219
220
221
# File 'lib/parse/live_query/subscription.rb', line 211

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



234
235
236
# File 'lib/parse/live_query/subscription.rb', line 234

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.



287
288
289
# File 'lib/parse/live_query/subscription.rb', line 287

def use_master_key?
  @use_master_key == true
end