Module: ActivityNotification::CascadingNotificationApi

Extended by:
ActiveSupport::Concern
Included in:
NotificationApi
Defined in:
lib/activity_notification/apis/cascading_notification_api.rb

Overview

Defines API for cascading notifications included in Notification model. Cascading notifications enable sequential delivery through different channels based on read status, with configurable time delays between each step.

Instance Method Summary collapse

Instance Method Details

#cascade_in_progress?Boolean

Checks if a cascading notification is currently in progress for this notification This is a helper method that checks if there are scheduled jobs for this notification

Returns:

  • (Boolean)

    true if cascade jobs are scheduled (this is a best-effort check)



154
155
156
157
158
159
# File 'lib/activity_notification/apis/cascading_notification_api.rb', line 154

def cascade_in_progress?
  # This is a best-effort check that returns false by default
  # In production, you might want to track this state differently
  # (e.g., in Redis, database flag, or by querying the job queue)
  false
end

#cascade_notify(cascade_config, options = {}) ⇒ Boolean

Starts a cascading notification chain with the specified configuration. The chain will automatically check the read status before each step and only proceed if the notification remains unread.

Examples:

Simple cascade with Slack then email

notification.cascade_notify([
  { delay: 10.minutes, target: :slack },
  { delay: 10.minutes, target: :email }
])

Cascade with custom options for each target

notification.cascade_notify([
  { delay: 5.minutes, target: :slack, options: { channel: '#alerts' } },
  { delay: 10.minutes, target: :amazon_sns, options: { subject: 'Urgent' } },
  { delay: 15.minutes, target: :email }
])

Parameters:

  • cascade_config (Array<Hash>)

    Array of cascade step configurations

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

    Additional options for cascade

Options Hash (cascade_config):

  • :delay (ActiveSupport::Duration)

    Required. Time to wait before this step

  • :target (Symbol, String)

    Required. Name of the optional target (e.g., :slack, :email)

  • :options (Hash)

    Optional. Parameters to pass to the optional target

Options Hash (options):

  • :validate (Boolean) — default: true

    Whether to validate the cascade configuration

  • :trigger_first_immediately (Boolean) — default: false

    Whether to trigger the first target immediately without delay

Returns:

  • (Boolean)

    true if cascade was initiated successfully, false otherwise

Raises:

  • (ArgumentError)

    if cascade_config is invalid and :validate is true



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/activity_notification/apis/cascading_notification_api.rb', line 34

def cascade_notify(cascade_config, options = {})
  validate = options.fetch(:validate, true)
  trigger_first_immediately = options.fetch(:trigger_first_immediately, false)
  
  # Validate configuration if requested
  if validate
    validation_result = validate_cascade_config(cascade_config)
    unless validation_result[:valid]
      raise ArgumentError, "Invalid cascade configuration: #{validation_result[:errors].join(', ')}"
    end
  end
  
  # Return false if cascade config is empty
  return false if cascade_config.blank?
  
  # Return false if notification is already opened
  return false if opened?
  
  if defined?(ActiveJob) && defined?(ActivityNotification::CascadingNotificationJob) && 
     ActivityNotification::CascadingNotificationJob.respond_to?(:perform_later)
    if trigger_first_immediately && cascade_config.any?
      # Trigger first target immediately
      first_step = cascade_config.first
      target_name = first_step[:target] || first_step['target']
      target_options = first_step[:options] || first_step['options'] || {}
      
      # Perform the first step synchronously
      perform_cascade_step(target_name, target_options)
      
      # Schedule remaining steps if any
      if cascade_config.length > 1
        remaining_config = cascade_config[1..-1]
        first_delay = remaining_config.first[:delay] || remaining_config.first['delay']
        
        if first_delay.present?
          ActivityNotification::CascadingNotificationJob
            .set(wait: first_delay)
            .perform_later(id, remaining_config, 0)
        end
      end
    else
      # Schedule first step with its configured delay
      first_step = cascade_config.first
      first_delay = first_step[:delay] || first_step['delay']
      
      if first_delay.present?
        ActivityNotification::CascadingNotificationJob
          .set(wait: first_delay)
          .perform_later(id, cascade_config, 0)
      else
        # If no delay specified for first step, trigger immediately
        ActivityNotification::CascadingNotificationJob
          .perform_later(id, cascade_config, 0)
      end
    end
    
    true
  else
    Rails.logger.error("ActiveJob or CascadingNotificationJob not available for cascading notifications")
    false
  end
end

#validate_cascade_config(cascade_config) ⇒ Hash

Validates a cascade configuration array

Parameters:

  • cascade_config (Array<Hash>)

    The configuration to validate

Returns:

  • (Hash)

    Hash with :valid (Boolean) and :errors (Array<String>) keys



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/activity_notification/apis/cascading_notification_api.rb', line 101

def validate_cascade_config(cascade_config)
  errors = []
  
  if cascade_config.nil?
    errors << "cascade_config cannot be nil"
    return { valid: false, errors: errors }
  end
  
  unless cascade_config.is_a?(Array)
    errors << "cascade_config must be an Array"
    return { valid: false, errors: errors }
  end
  
  if cascade_config.empty?
    errors << "cascade_config cannot be empty"
  end
  
  cascade_config.each_with_index do |step, index|
    unless step.is_a?(Hash)
      errors << "Step #{index} must be a Hash"
      next
    end
    
    # Check for required target parameter
    target = step[:target] || step['target']
    if target.nil?
      errors << "Step #{index} missing required :target parameter"
    elsif !target.is_a?(Symbol) && !target.is_a?(String)
      errors << "Step #{index} :target must be a Symbol or String"
    end
    
    # Check for delay parameter (only required for steps after the first if not using trigger_first_immediately)
    delay = step[:delay] || step['delay']
    if delay.nil?
      errors << "Step #{index} missing :delay parameter"
    elsif !delay.respond_to?(:from_now) && !delay.is_a?(Numeric)
      errors << "Step #{index} :delay must be an ActiveSupport::Duration or Numeric (seconds)"
    end
    
    # Check options if present
    options = step[:options] || step['options']
    if options.present? && !options.is_a?(Hash)
      errors << "Step #{index} :options must be a Hash"
    end
  end
  
  { valid: errors.empty?, errors: errors }
end