Module: Sinatra::Scheduled::ClassMethods

Defined in:
lib/sinatra/scheduled.rb

Instance Method Summary collapse

Instance Method Details

#dispatch_scheduled(event, js_env = nil, js_ctx = nil) ⇒ Object

Dispatcher entry point — called by Cloudflare::Scheduled with a Cloudflare::ScheduledEvent and the JS env / ctx objects. Returns a Hash with ‘fired`, `total`, `errors` for diagnostics.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/sinatra/scheduled.rb', line 167

def dispatch_scheduled(event, js_env = nil, js_ctx = nil)
  jobs = scheduled_jobs_for(event.cron)
  results = []
  i = 0
  # `while` instead of `each` keeps the per-iteration begin/rescue
  # straightforward under Opal's `# await: true` translation —
  # each iteration's `__await__` is awaited inline rather than
  # through a yielded async block (which has had subtle issues
  # with rescue propagation in Opal).
  while i < jobs.length
    job = jobs[i]
    start = Time.now.to_f
    begin
      # invoke_scheduled_job is an async method (it's defined
      # inside a `# await: true` file and may itself await an
      # inner Promise from the user block). Calling it returns
      # a Promise — we MUST `__await__` it here so:
      #   (a) downstream code sees fully-applied side effects,
      #   (b) the rescue below catches Promise rejections that
      #       propagate as Ruby exceptions.
      #
      # The literal `__await__` token is what Opal scans for to
      # emit a JS `await`; calling a helper that internally does
      # `__await__` is NOT enough, because the helper's return
      # value is itself a Promise that the caller would have to
      # `__await__` again.
      promise = invoke_scheduled_job(job, event, js_env, js_ctx)
      if `(#{promise} != null && typeof #{promise}.then === 'function')`
        promise.__await__
      end
      results << {
        'name'     => job.name,
        'cron'     => job.cron,
        'ok'       => true,
        'duration' => Time.now.to_f - start
      }
    rescue ::Exception => e
      results << {
        'name'     => job.name,
        'cron'     => job.cron,
        'ok'       => false,
        'error'    => "#{e.class}: #{e.message}",
        'duration' => Time.now.to_f - start
      }
    end
    i += 1
  end
  { 'fired' => results.size, 'total' => scheduled_jobs.size, 'results' => results }
end

#schedule(cron, name: nil, match: nil, &block) ⇒ Object

Register a cron block.

schedule '*/5 * * * *' do |event|
  ...
end

Options:

:name      — human label for logging (default: the cron string)
:match     — proc(cron_string) returning truthy if this job
             should fire. Defaults to exact-string equality.

Raises:

  • (ArgumentError)


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
149
150
151
152
153
154
155
156
157
# File 'lib/sinatra/scheduled.rb', line 115

def schedule(cron, name: nil, match: nil, &block)
  raise ArgumentError, 'schedule requires a block' unless block
  cron_str = cron.to_s
  raise ArgumentError, 'cron expression must be non-empty' if cron_str.empty?
  # Cheap structural sanity-check: 5 or 6 whitespace-separated
  # fields. Cloudflare allows the standard 5-field form.
  fields = cron_str.split(/\s+/)
  unless [5, 6].include?(fields.length)
    raise ArgumentError, "cron expression must have 5 or 6 fields, got #{fields.length}: #{cron_str.inspect}"
  end
  # Fail loudly at registration time if a non-callable `match:`
  # was passed — otherwise the failure would surface only when
  # the cron actually fires (as a NoMethodError during dispatch),
  # which is a much worse debugging experience.
  if !match.nil? && !match.respond_to?(:call)
    raise ArgumentError, "match: must respond to #call (got #{match.class})"
  end

  loc = block.respond_to?(:source_location) ? block.source_location : nil
  file = loc.is_a?(Array) ? loc[0] : nil
  line = loc.is_a?(Array) ? loc[1] : nil

  # Convert the block into an UnboundMethod bound to
  # ScheduledContext. `define_method` is what triggers Opal's
  # `# await: true` machinery to wrap the body as an async
  # function — without this step, `kv.get(...).__await__` would
  # never resolve because the surrounding scope isn't async.
  # See the Job class comment for the full rationale.
  method_name = "__scheduled_#{cron_str.object_id}_#{scheduled_jobs.length}".to_sym
  ScheduledContext.send(:define_method, method_name, &block)
  unbound = ScheduledContext.instance_method(method_name)
  ScheduledContext.send(:remove_method, method_name)

  scheduled_jobs << Job.new(
    cron: cron_str,
    name: name,
    block: block,
    unbound_method: unbound,
    match_proc: match,
    file: file,
    line: line
  )
end

#scheduled_jobsObject

Returns the per-class array of registered Job instances. Stored in a class instance variable, so subclasses do not inherit the parent’s jobs unless that behavior is implemented explicitly (e.g. via an ‘inherited` hook copying the parent’s ‘@scheduled_jobs`). The current behavior keeps each Sinatra subclass’s schedule registry independent.



101
102
103
# File 'lib/sinatra/scheduled.rb', line 101

def scheduled_jobs
  @scheduled_jobs ||= []
end

#scheduled_jobs_for(cron_string) ⇒ Object

Returns all jobs that match the given cron string.



160
161
162
# File 'lib/sinatra/scheduled.rb', line 160

def scheduled_jobs_for(cron_string)
  scheduled_jobs.select { |j| j.matches?(cron_string) }
end