stepper_motor 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +51 -0
  4. data/CHANGELOG.md +77 -2
  5. data/Gemfile +11 -0
  6. data/README.md +13 -374
  7. data/Rakefile +21 -3
  8. data/bin/test +5 -0
  9. data/lib/generators/install_generator.rb +6 -1
  10. data/lib/generators/stepper_motor_migration_003.rb.erb +6 -0
  11. data/lib/generators/stepper_motor_migration_004.rb.erb +26 -0
  12. data/lib/stepper_motor/forward_scheduler.rb +8 -4
  13. data/lib/stepper_motor/journey/flow_control.rb +58 -0
  14. data/lib/stepper_motor/journey/recovery.rb +34 -0
  15. data/lib/stepper_motor/journey.rb +85 -84
  16. data/lib/stepper_motor/perform_step_job_v2.rb +2 -2
  17. data/lib/stepper_motor/recover_stuck_journeys_job_v1.rb +3 -1
  18. data/lib/stepper_motor/step.rb +70 -5
  19. data/lib/stepper_motor/version.rb +1 -1
  20. data/lib/stepper_motor.rb +0 -1
  21. data/lib/tasks/stepper_motor_tasks.rake +8 -0
  22. data/manual/MANUAL.md +538 -0
  23. data/rbi/stepper_motor.rbi +459 -0
  24. data/sig/stepper_motor.rbs +406 -3
  25. data/stepper_motor.gemspec +49 -0
  26. data/test/dummy/Rakefile +8 -0
  27. data/test/dummy/app/assets/stylesheets/application.css +1 -0
  28. data/test/dummy/app/controllers/application_controller.rb +6 -0
  29. data/test/dummy/app/helpers/application_helper.rb +4 -0
  30. data/test/dummy/app/jobs/application_job.rb +9 -0
  31. data/test/dummy/app/mailers/application_mailer.rb +6 -0
  32. data/test/dummy/app/models/application_record.rb +5 -0
  33. data/test/dummy/app/views/layouts/application.html.erb +27 -0
  34. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  35. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  36. data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
  37. data/test/dummy/app/views/pwa/service-worker.js +26 -0
  38. data/test/dummy/bin/dev +2 -0
  39. data/test/dummy/bin/rails +4 -0
  40. data/test/dummy/bin/rake +4 -0
  41. data/test/dummy/bin/setup +34 -0
  42. data/test/dummy/config/application.rb +28 -0
  43. data/test/dummy/config/boot.rb +7 -0
  44. data/test/dummy/config/cable.yml +10 -0
  45. data/test/dummy/config/database.yml +32 -0
  46. data/test/dummy/config/environment.rb +7 -0
  47. data/test/dummy/config/environments/development.rb +71 -0
  48. data/test/dummy/config/environments/production.rb +91 -0
  49. data/test/dummy/config/environments/test.rb +55 -0
  50. data/test/dummy/config/initializers/content_security_policy.rb +27 -0
  51. data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
  52. data/test/dummy/config/initializers/inflections.rb +18 -0
  53. data/test/dummy/config/initializers/stepper_motor.rb +3 -0
  54. data/test/dummy/config/locales/en.yml +31 -0
  55. data/test/dummy/config/puma.rb +40 -0
  56. data/test/dummy/config/routes.rb +16 -0
  57. data/test/dummy/config/storage.yml +34 -0
  58. data/test/dummy/config.ru +8 -0
  59. data/test/dummy/db/migrate/20250520094921_stepper_motor_migration_001.rb +38 -0
  60. data/test/dummy/db/migrate/20250520094922_stepper_motor_migration_002.rb +8 -0
  61. data/test/dummy/db/migrate/20250522212312_stepper_motor_migration_003.rb +7 -0
  62. data/test/dummy/db/migrate/20250525110812_stepper_motor_migration_004.rb +28 -0
  63. data/test/dummy/db/schema.rb +37 -0
  64. data/test/dummy/public/400.html +114 -0
  65. data/test/dummy/public/404.html +114 -0
  66. data/test/dummy/public/406-unsupported-browser.html +114 -0
  67. data/test/dummy/public/422.html +114 -0
  68. data/test/dummy/public/500.html +114 -0
  69. data/test/dummy/public/icon.png +0 -0
  70. data/test/dummy/public/icon.svg +3 -0
  71. data/test/side_effects_helper.rb +67 -0
  72. data/test/stepper_motor/cyclic_scheduler_test.rb +77 -0
  73. data/{spec/stepper_motor/forward_scheduler_spec.rb → test/stepper_motor/forward_scheduler_test.rb} +9 -10
  74. data/test/stepper_motor/journey/exception_handling_test.rb +89 -0
  75. data/test/stepper_motor/journey/flow_control_test.rb +78 -0
  76. data/test/stepper_motor/journey/idempotency_test.rb +65 -0
  77. data/test/stepper_motor/journey/step_definition_test.rb +187 -0
  78. data/test/stepper_motor/journey/uniqueness_test.rb +48 -0
  79. data/test/stepper_motor/journey_test.rb +352 -0
  80. data/{spec/stepper_motor/recover_stuck_journeys_job_spec.rb → test/stepper_motor/recover_stuck_journeys_job_test.rb} +14 -14
  81. data/{spec/stepper_motor/recovery_spec.rb → test/stepper_motor/recovery_test.rb} +27 -27
  82. data/test/stepper_motor/test_helper_test.rb +44 -0
  83. data/test/stepper_motor_test.rb +9 -0
  84. data/test/test_helper.rb +46 -0
  85. metadata +120 -24
  86. data/.rspec +0 -3
  87. data/.ruby-version +0 -1
  88. data/.standard.yml +0 -4
  89. data/.yardopts +0 -1
  90. data/spec/helpers/side_effects.rb +0 -85
  91. data/spec/spec_helper.rb +0 -90
  92. data/spec/stepper_motor/cyclic_scheduler_spec.rb +0 -68
  93. data/spec/stepper_motor/generator_spec.rb +0 -16
  94. data/spec/stepper_motor/journey_spec.rb +0 -401
  95. data/spec/stepper_motor/test_helper_spec.rb +0 -48
  96. data/spec/stepper_motor_spec.rb +0 -7
@@ -1,4 +1,407 @@
1
+ # StepperMotor is a module for building multi-step flows where steps are sequential and only
2
+ # ever progress forward. The building block of StepperMotor is StepperMotor::Journey
1
3
  module StepperMotor
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
4
+ VERSION: untyped
5
+
6
+ class Error < StandardError
7
+ end
8
+
9
+ class JourneyNotPersisted < StepperMotor::Error
10
+ end
11
+
12
+ class StepConfigurationError < ArgumentError
13
+ end
14
+
15
+ # Describes a step in a journey. These objects get stored inside the `step_definitions`
16
+ # array of the Journey subclass. When the step gets performed, the block passed to the
17
+ # constructor will be instance_exec'd with the Journey model being the context
18
+ class Step
19
+ # sord omit - no YARD type given for "seq:", using untyped
20
+ # sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
21
+ # Creates a new step definition
22
+ #
23
+ # _@param_ `name` — the name of the Step
24
+ #
25
+ # _@param_ `wait` — the amount of time to wait before entering the step
26
+ #
27
+ # _@param_ `on_exception` — the action to take if an exception occurs when performing the step. The possible values are: * `:cancel!` - cancels the Journey and re-raises the exception. The Journey will be persisted before re-raising. * `:reattempt!` - reattempts the Journey and re-raises the exception. The Journey will be persisted before re-raising.
28
+ def initialize: (
29
+ name: (String | Symbol),
30
+ seq: untyped,
31
+ on_exception: Symbol,
32
+ ?wait: (Numeric | ActiveSupport::Duration)
33
+ ) -> void
34
+
35
+ # Performs the step on the passed Journey, wrapping the step with the required context.
36
+ #
37
+ # _@param_ `journey` — the journey to perform the step in. If a `step_block` is passed in, it is going to be executed in the context of the journey using `instance_exec`. If only the name of the step has been provided, an accordingly named public method on the journey will be called
38
+ #
39
+ # _@return_ — void
40
+ def perform_in_context_of: (StepperMotor::Journey journey) -> untyped
41
+
42
+ # _@return_ — the name of the step or method to call on the Journey
43
+ attr_reader name: String
44
+
45
+ # sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
46
+ # _@return_ — how long to wait before performing the step
47
+ attr_reader wait: (Numeric | ActiveSupport::Duration)
48
+
49
+ # sord omit - no YARD type given for :seq, using untyped
50
+ attr_reader seq: untyped
51
+
52
+ class MissingDefinition < NoMethodError
53
+ end
54
+ end
55
+
56
+ # A Journey is the main building block of StepperMotor. You create a journey to guide a particular model
57
+ # ("hero") through a sequence of steps. Any of your model can be the hero and have multiple Journeys. To create
58
+ # your own Journey, subclass the `StepperMotor::Journey` class and define your steps. For example, a drip mail
59
+ # campaign can look like this:
60
+ #
61
+ #
62
+ # class ResubscribeCampaign < StepperMotor::Journey
63
+ # step do
64
+ # ReinviteMailer.with(recipient: hero).deliver_later
65
+ # end
66
+ #
67
+ # step wait: 3.days do
68
+ # cancel! if hero.active?
69
+ # ReinviteMailer.with(recipient: hero).deliver_later
70
+ # end
71
+ #
72
+ # step wait: 3.days do
73
+ # cancel! if hero.active?
74
+ # ReinviteMailer.with(recipient: hero).deliver_later
75
+ # end
76
+ #
77
+ # step wait: 3.days do
78
+ # cancel! if hero.active?
79
+ # hero.close_account!
80
+ # end
81
+ # end
82
+ #
83
+ # Creating a record for the Journey (just using `create!`) will instantly send your hero on their way:
84
+ #
85
+ # ResubscribeCampaign.create!(hero: current_account)
86
+ #
87
+ # To stop the journey forcibly, delete it from your database - or call `cancel!` within any of the steps.
88
+ class Journey < ActiveRecord::Base
89
+ include StepperMotor::Journey::FlowControl
90
+ include StepperMotor::Journey::Recovery
91
+ STATES: untyped
92
+
93
+ # sord omit - no YARD return type given, using untyped
94
+ # Alias for the class attribute, for brevity
95
+ #
96
+ # _@see_ `Journey.step_definitions`
97
+ def step_definitions: () -> untyped
98
+
99
+ # sord duck - #to_f looks like a duck type, replacing with untyped
100
+ # sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
101
+ # sord duck - #to_f looks like a duck type, replacing with untyped
102
+ # sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
103
+ # Defines a step in the journey.
104
+ # Steps are stacked top to bottom and get performed in sequence.
105
+ #
106
+ # _@param_ `name` — the name of the step. If none is provided, a name will be automatically generated based on the position of the step in the list of `step_definitions`. The name can also be used to call a method on the `Journey` instead of calling the provided block.
107
+ #
108
+ # _@param_ `wait` — the amount of time this step should wait before getting performed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time, and the `next_step_to_be_performed_at` attribute will be set to the current time plus the wait duration. Mutually exclusive with `after:`
109
+ #
110
+ # _@param_ `after` — the amount of time this step should wait before getting performed including all the previous waits. This allows you to set the wait time based on the time after the journey started, as opposed to when the previous step has completed. When the journey gets scheduled, the triggering job is going to be delayed by this amount of time _minus the `wait` values of the preceding steps, and the `next_step_to_be_performed_at` attribute will be set to the current time. The `after` value gets converted into the `wait` value and passed to the step definition. Mutually exclusive with `wait:`
111
+ #
112
+ # _@param_ `on_exception` — See {StepperMotor::Step#on_exception}
113
+ #
114
+ # _@param_ `additional_step_definition_options` — Any remaining options get passed to `StepperMotor::Step.new` as keyword arguments.
115
+ #
116
+ # _@return_ — the step definition that has been created
117
+ def self.step: (
118
+ ?String? name,
119
+ ?wait: (Float | untyped | ActiveSupport::Duration)?,
120
+ ?after: (Float | untyped | ActiveSupport::Duration)?,
121
+ ?on_exception: Symbol,
122
+ **untyped additional_step_definition_options
123
+ ) -> StepperMotor::Step
124
+
125
+ # sord warn - "StepperMotor::Step?" does not appear to be a type
126
+ # Returns the `Step` object for a named step. This is used when performing a step, but can also
127
+ # be useful in other contexts.
128
+ #
129
+ # _@param_ `by_step_name` — the name of the step to find
130
+ def self.lookup_step_definition: ((Symbol | String) by_step_name) -> SORD_ERROR_StepperMotorStep
131
+
132
+ # sord omit - no YARD type given for "by_step_name", using untyped
133
+ # sord omit - no YARD return type given, using untyped
134
+ # Alias for the class method, for brevity
135
+ #
136
+ # _@see_ `Journey.lookup_step_definition`
137
+ def lookup_step_definition: (untyped by_step_name) -> untyped
138
+
139
+ # Performs the next step in the journey. Will check whether any other process has performed the step already
140
+ # and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
141
+ #
142
+ # After setting the state, it will determine the next step to perform, and perform it. Depending on the outcome of
143
+ # the step another `PerformStepJob` may get enqueued. If the journey ends here, the journey record will set its state
144
+ # to 'finished'.
145
+ #
146
+ # _@param_ `idempotency_key` — If provided, the step will only be performed if the idempotency key matches the current idempotency key. This ensures that the only the triggering job that was scheduled for this step can trigger the step and not any other.
147
+ def perform_next_step!: (?idempotency_key: String?) -> void
148
+
149
+ # sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
150
+ def time_remaining_until_final_step: () -> ActiveSupport::Duration
151
+
152
+ # sord omit - no YARD type given for "next_step_definition", using untyped
153
+ # sord omit - no YARD type given for "wait:", using untyped
154
+ # sord omit - no YARD return type given, using untyped
155
+ def set_next_step_and_enqueue: (untyped next_step_definition, ?wait: untyped) -> untyped
156
+
157
+ # sord omit - no YARD return type given, using untyped
158
+ def logger: () -> untyped
159
+
160
+ # sord omit - no YARD type given for "step_name", using untyped
161
+ # sord omit - no YARD return type given, using untyped
162
+ def after_locking_for_step: (untyped step_name) -> untyped
163
+
164
+ # sord omit - no YARD type given for "step_name", using untyped
165
+ # sord omit - no YARD type given for "exception", using untyped
166
+ # sord omit - no YARD return type given, using untyped
167
+ def after_performing_step_with_exception: (untyped step_name, untyped exception) -> untyped
168
+
169
+ # sord omit - no YARD type given for "step_name", using untyped
170
+ # sord omit - no YARD return type given, using untyped
171
+ def before_step_starts: (untyped step_name) -> untyped
172
+
173
+ # sord omit - no YARD type given for "step_name", using untyped
174
+ # sord omit - no YARD return type given, using untyped
175
+ def after_performing_step_without_exception: (untyped step_name) -> untyped
176
+
177
+ # sord omit - no YARD return type given, using untyped
178
+ def schedule!: () -> untyped
179
+
180
+ # sord omit - no YARD return type given, using untyped
181
+ def recover!: () -> untyped
182
+
183
+ # Is a convenient way to end a hero's journey. Imagine you enter a step where you are inviting a user
184
+ # to rejoin the platform, and are just about to send them an email - but they have already joined. You
185
+ # can therefore cancel their journey. Canceling bails you out of the `step`-defined block and sets the journey record to the `canceled` state.
186
+ #
187
+ # Calling `cancel!` within a step will abort the execution of the current step.
188
+ #
189
+ # _@return_ — void
190
+ def cancel!: () -> untyped
191
+
192
+ # sord omit - no YARD type given for "wait:", using untyped
193
+ # Inside a step it is possible to ask StepperMotor to retry to start the step at a later point in time. Maybe now is an inconvenient moment
194
+ # (are you about to send a push notification at 3AM perhaps?). The `wait:` parameter specifies how long to defer reattempting the step for.
195
+ # Reattempting will resume the step from the beginning, so the step should be idempotent.
196
+ #
197
+ # `reattempt!` may only be called within a step.
198
+ #
199
+ # _@return_ — void
200
+ def reattempt!: (?wait: untyped) -> untyped
201
+
202
+ # Is used to pause a Journey at any point. The "paused" state is similar to the "ready" state, except that "perform_next_step!" on the
203
+ # journey will do nothing - even if it is scheduled to be performed. Pausing a Journey can be useful in the following situations:
204
+ #
205
+ # * The hero of the journey is in a compliance procedure, and their Journeys should not continue
206
+ # * The external resource a Journey will be calling is not available
207
+ # * There is a bug in the Journey implementation and you need some time to get it fixed without canceling or recreating existing Journeys
208
+ #
209
+ # Calling `pause!` within a step will abort the execution of the current step.
210
+ #
211
+ # _@return_ — void
212
+ def pause!: () -> untyped
213
+
214
+ # Is used to resume a paused Journey. It places the Journey into the `ready` state and schedules the job to perform that step.
215
+ #
216
+ # Calling `resume!` is only permitted outside of a step
217
+ #
218
+ # _@return_ — void
219
+ def resume!: () -> untyped
220
+
221
+ module Recovery
222
+ extend ActiveSupport::Concern
223
+
224
+ # sord omit - no YARD return type given, using untyped
225
+ def recover!: () -> untyped
226
+ end
227
+
228
+ module FlowControl
229
+ # Is a convenient way to end a hero's journey. Imagine you enter a step where you are inviting a user
230
+ # to rejoin the platform, and are just about to send them an email - but they have already joined. You
231
+ # can therefore cancel their journey. Canceling bails you out of the `step`-defined block and sets the journey record to the `canceled` state.
232
+ #
233
+ # Calling `cancel!` within a step will abort the execution of the current step.
234
+ #
235
+ # _@return_ — void
236
+ def cancel!: () -> untyped
237
+
238
+ # sord omit - no YARD type given for "wait:", using untyped
239
+ # Inside a step it is possible to ask StepperMotor to retry to start the step at a later point in time. Maybe now is an inconvenient moment
240
+ # (are you about to send a push notification at 3AM perhaps?). The `wait:` parameter specifies how long to defer reattempting the step for.
241
+ # Reattempting will resume the step from the beginning, so the step should be idempotent.
242
+ #
243
+ # `reattempt!` may only be called within a step.
244
+ #
245
+ # _@return_ — void
246
+ def reattempt!: (?wait: untyped) -> untyped
247
+
248
+ # Is used to pause a Journey at any point. The "paused" state is similar to the "ready" state, except that "perform_next_step!" on the
249
+ # journey will do nothing - even if it is scheduled to be performed. Pausing a Journey can be useful in the following situations:
250
+ #
251
+ # * The hero of the journey is in a compliance procedure, and their Journeys should not continue
252
+ # * The external resource a Journey will be calling is not available
253
+ # * There is a bug in the Journey implementation and you need some time to get it fixed without canceling or recreating existing Journeys
254
+ #
255
+ # Calling `pause!` within a step will abort the execution of the current step.
256
+ #
257
+ # _@return_ — void
258
+ def pause!: () -> untyped
259
+
260
+ # Is used to resume a paused Journey. It places the Journey into the `ready` state and schedules the job to perform that step.
261
+ #
262
+ # Calling `resume!` is only permitted outside of a step
263
+ #
264
+ # _@return_ — void
265
+ def resume!: () -> untyped
266
+ end
267
+ end
268
+
269
+ class Railtie < Rails::Railtie
270
+ end
271
+
272
+ module TestHelper
273
+ # Allows running a given Journey to completion, skipping across the waiting periods.
274
+ # This is useful to evaluate all side effects of a Journey. The helper will ensure
275
+ # that the number of steps performed is equal to the number of steps defined - this way
276
+ # it will not enter into an endless loop. If, after completing all the steps, the journey
277
+ # has neither canceled nor finished, an exception will be raised.
278
+ #
279
+ # _@param_ `journey` — the journey to speedrun
280
+ #
281
+ # _@return_ — void
282
+ def speedrun_journey: (StepperMotor::Journey journey) -> untyped
283
+
284
+ # Performs the named step of the journey without waiting for the time to perform the step.
285
+ #
286
+ # _@param_ `journey` — the journey to speedrun
287
+ #
288
+ # _@param_ `step_name` — the name of the step to run
289
+ #
290
+ # _@return_ — void
291
+ def immediately_perform_single_step: (StepperMotor::Journey journey, Symbol step_name) -> untyped
292
+ end
293
+
294
+ # The generator is used to install StepperMotor. It adds an example Journey, a configing
295
+ # initializer and the migration that creates tables.
296
+ # Run it with `bin/rails g stepper_motor:install` in your console.
297
+ class InstallGenerator < Rails::Generators::Base
298
+ include ActiveRecord::Generators::Migration
299
+ UUID_MESSAGE: untyped
300
+
301
+ # sord omit - no YARD return type given, using untyped
302
+ # Generates monolithic migration file that contains all database changes.
303
+ def create_migration_file: () -> untyped
304
+
305
+ # sord omit - no YARD return type given, using untyped
306
+ def create_initializer: () -> untyped
307
+
308
+ def uuid_fk?: () -> bool
309
+
310
+ # sord omit - no YARD return type given, using untyped
311
+ def migration_version: () -> untyped
312
+ end
313
+
314
+ # The cyclic scheduler is designed to be run regularly via a cron job. On every
315
+ # cycle, it is going to look for Journeys which are going to come up for step execution
316
+ # before the next cycle is supposed to run. Then it is going to enqueue jobs to perform
317
+ # steps on those journeys. Since the scheduler gets run at a discrete interval, but we
318
+ # still them to be processed on time, if we only picked up the journeys which have the
319
+ # step execution time set to now or earlier, we will always have delays. This is why
320
+ # this scheduler enqueues jobs for journeys whose time to run is between now and the
321
+ # next cycle.
322
+ #
323
+ # Once the job gets created, it then gets enqueued and gets picked up by the ActiveJob
324
+ # worker normally. If you are using SQS, which has a limit of 900 seconds for the `wait:`
325
+ # value, you need to run the scheduler at least (!) every 900 seconds, and preferably
326
+ # more frequently (for example, once every 5 minutes). This scheduler is also going to be
327
+ # more gentle with ActiveJob adapters that may get slower with large queue depths, such as
328
+ # good_job. This scheduler is a good fit if you are using an ActiveJob adapter which:
329
+ #
330
+ # * Does not allow easy introspection of jobs in the future (like Redis-based queues)
331
+ # * Limits the value of the `wait:` parameter
332
+ #
333
+ # The scheduler needs to be configured in your cron table.
334
+ class CyclicScheduler < StepperMotor::ForwardScheduler
335
+ # sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
336
+ # Creates a new scheduler. The scheduler needs to know how frequently it is going to be running -
337
+ # you define that frequency when you configure your cron job that calls `run_scheduling_cycle`. Journeys which
338
+ # have to perform their steps between the runs of the cycles will generate jobs. The more frequent the scheduling
339
+ # cycle, the fewer jobs are going to be created per cycle.
340
+ #
341
+ # _@param_ `cycle_duration` — how frequently the scheduler runs
342
+ def initialize: (cycle_duration: ActiveSupport::Duration) -> void
343
+
344
+ # Run a scheduling cycle. This should be called from your ActiveJob that runs on a regular Cron cadence. Ideally you
345
+ # would call the instance of the scheduler configured for the whole StepperMotor (so that the `cycle_duration` gets
346
+ # correctly applied, as it is necessary to pick the journeys to step). Normally, you would do this:
347
+ def run_scheduling_cycle: () -> void
348
+
349
+ # sord omit - no YARD type given for "journey", using untyped
350
+ # sord omit - no YARD return type given, using untyped
351
+ def schedule: (untyped journey) -> untyped
352
+
353
+ class RunSchedulingCycleJob < ActiveJob::Base
354
+ # sord omit - no YARD return type given, using untyped
355
+ def perform: () -> untyped
356
+ end
357
+ end
358
+
359
+ class PerformStepJob < ActiveJob::Base
360
+ # sord omit - no YARD type given for "journey_gid", using untyped
361
+ # sord omit - no YARD return type given, using untyped
362
+ def perform: (untyped journey_gid) -> untyped
363
+ end
364
+
365
+ # The forward scheduler enqueues a job for every Journey that
366
+ # gets sent to the `#schedule`. The job is then stored in the queue
367
+ # and gets picked up by the ActiveJob worker normally. This is the simplest
368
+ # option if your ActiveJob adapter supports far-ahead scheduling. Some adapters,
369
+ # such as SQS, have limitations regarding the maximum delay after which a message
370
+ # will become visible. For SQS, the limit is 900 seconds. If the job is further in the future,
371
+ # it is likely going to fail to get enqueued. If you are working with a queue adapter that:
372
+ #
373
+ # * Does not allow easy introspection of jobs in the future (like Redis-based queues)
374
+ # * Limits the value of the `wait:` parameter
375
+ #
376
+ # this scheduler may not be a good fit for you, and you will need to use the {CyclicScheduler} instead.
377
+ # Note that this scheduler is also likely to populate your queue with a high number of "far out"
378
+ # jobs to be performed in the future. Different ActiveJob adapters are known to have varying
379
+ # performance depending on the number of jobs in the queue. For example, good_job is known to
380
+ # struggle a bit if the queue contains a large number of jobs (even if those jobs are not yet
381
+ # scheduled to be performed). For good_job the {CyclicScheduler} is also likely to be a better option.
382
+ class ForwardScheduler
383
+ # sord omit - no YARD type given for "journey", using untyped
384
+ # sord omit - no YARD return type given, using untyped
385
+ def schedule: (untyped journey) -> untyped
386
+ end
387
+
388
+ class PerformStepJobV2 < ActiveJob::Base
389
+ # sord omit - no YARD type given for "journey_id:", using untyped
390
+ # sord omit - no YARD type given for "journey_class_name:", using untyped
391
+ # sord omit - no YARD type given for "idempotency_key:", using untyped
392
+ # sord omit - no YARD return type given, using untyped
393
+ def perform: (journey_id: untyped, journey_class_name: untyped, ?idempotency_key: untyped) -> untyped
394
+ end
395
+
396
+ # The purpose of this job is to find journeys which have, for whatever reason, remained in the
397
+ # `performing` state for far longer than the journey is supposed to. At the moment it assumes
398
+ # any journey that stayed in `performing` for longer than 1 hour has hung. Add this job to your
399
+ # cron table and perform it regularly.
400
+ class RecoverStuckJourneysJobV1 < ActiveJob::Base
401
+ DEFAULT_STUCK_FOR: untyped
402
+
403
+ # sord omit - no YARD type given for "stuck_for:", using untyped
404
+ # sord omit - no YARD return type given, using untyped
405
+ def perform: (?stuck_for: untyped) -> untyped
406
+ end
407
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/stepper_motor/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "stepper_motor"
7
+ spec.version = StepperMotor::VERSION
8
+ spec.authors = ["Julik Tarkhanov"]
9
+ spec.email = ["me@julik.nl"]
10
+ spec.license = "LGPL"
11
+
12
+ spec.summary = "Effortless step workflows that embed nicely inside Rails"
13
+ spec.description = "Step workflows for Rails/ActiveRecord"
14
+ spec.homepage = "https://steppermotor.dev"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/stepper-motor/stepper_motor"
21
+ spec.metadata["changelog_uri"] = "https://github.com/stepper-motor/stepper_motor/blob/main/CHANGELOG.md"
22
+
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ File.basename(f).start_with?(".")
26
+ end
27
+ end
28
+
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency "activerecord", ">= 7"
34
+ spec.add_dependency "activejob"
35
+ spec.add_dependency "railties"
36
+ spec.add_dependency "globalid"
37
+
38
+ spec.add_development_dependency "minitest"
39
+ spec.add_development_dependency "rails", "~> 7.0"
40
+ spec.add_development_dependency "sqlite3"
41
+ spec.add_development_dependency "rake", "~> 13.0"
42
+ spec.add_development_dependency "rspec", "~> 3.0"
43
+ spec.add_development_dependency "rspec-rails"
44
+ spec.add_development_dependency "standard"
45
+ spec.add_development_dependency "magic_frozen_string_literal"
46
+ spec.add_development_dependency "yard"
47
+ spec.add_development_dependency "redcarpet" # needed for the yard gem to enable Github Flavored Markdown
48
+ spec.add_development_dependency "sord"
49
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
4
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5
+
6
+ require_relative "config/application"
7
+
8
+ Rails.application.load_tasks
@@ -0,0 +1 @@
1
+ /* Application styles */
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationController < ActionController::Base
4
+ # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
5
+ allow_browser versions: :modern
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApplicationHelper
4
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationJob < ActiveJob::Base
4
+ # Automatically retry jobs that encountered a deadlock
5
+ # retry_on ActiveRecord::Deadlocked
6
+
7
+ # Most jobs are safe to ignore if the underlying records are no longer available
8
+ # discard_on ActiveJob::DeserializationError
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ primary_abstract_class
5
+ end
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for(:title) || "Dummy" %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+
11
+ <%= yield :head %>
12
+
13
+ <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
14
+ <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15
+
16
+ <link rel="icon" href="/icon.png" type="image/png">
17
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
18
+ <link rel="apple-touch-icon" href="/icon.png">
19
+
20
+ <%# Includes all stylesheet files in app/assets/stylesheets %>
21
+ <%= stylesheet_link_tag "application" %>
22
+ </head>
23
+
24
+ <body>
25
+ <%= yield %>
26
+ </body>
27
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "Dummy",
3
+ "icons": [
4
+ {
5
+ "src": "/icon.png",
6
+ "type": "image/png",
7
+ "sizes": "512x512"
8
+ },
9
+ {
10
+ "src": "/icon.png",
11
+ "type": "image/png",
12
+ "sizes": "512x512",
13
+ "purpose": "maskable"
14
+ }
15
+ ],
16
+ "start_url": "/",
17
+ "display": "standalone",
18
+ "scope": "/",
19
+ "description": "Dummy.",
20
+ "theme_color": "red",
21
+ "background_color": "red"
22
+ }
@@ -0,0 +1,26 @@
1
+ // Add a service worker for processing Web Push notifications:
2
+ //
3
+ // self.addEventListener("push", async (event) => {
4
+ // const { title, options } = await event.data.json()
5
+ // event.waitUntil(self.registration.showNotification(title, options))
6
+ // })
7
+ //
8
+ // self.addEventListener("notificationclick", function(event) {
9
+ // event.notification.close()
10
+ // event.waitUntil(
11
+ // clients.matchAll({ type: "window" }).then((clientList) => {
12
+ // for (let i = 0; i < clientList.length; i++) {
13
+ // let client = clientList[i]
14
+ // let clientPath = (new URL(client.url)).pathname
15
+ //
16
+ // if (clientPath == event.notification.data.path && "focus" in client) {
17
+ // return client.focus()
18
+ // }
19
+ // }
20
+ //
21
+ // if (clients.openWindow) {
22
+ // return clients.openWindow(event.notification.data.path)
23
+ // }
24
+ // })
25
+ // )
26
+ // })
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ exec "./bin/rails", "server", *ARGV
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path("../config/application", __dir__)
3
+ require_relative "../config/boot"
4
+ require "rails/commands"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../config/boot"
3
+ require "rake"
4
+ Rake.application.run
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require "fileutils"
3
+
4
+ APP_ROOT = File.expand_path("..", __dir__)
5
+
6
+ def system!(*args)
7
+ system(*args, exception: true)
8
+ end
9
+
10
+ FileUtils.chdir APP_ROOT do
11
+ # This script is a way to set up or update your development environment automatically.
12
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
13
+ # Add necessary setup steps to this file.
14
+
15
+ puts "== Installing dependencies =="
16
+ system("bundle check") || system!("bundle install")
17
+
18
+ # puts "\n== Copying sample files =="
19
+ # unless File.exist?("config/database.yml")
20
+ # FileUtils.cp "config/database.yml.sample", "config/database.yml"
21
+ # end
22
+
23
+ puts "\n== Preparing database =="
24
+ system! "bin/rails db:prepare"
25
+
26
+ puts "\n== Removing old logs and tempfiles =="
27
+ system! "bin/rails log:clear tmp:clear"
28
+
29
+ unless ARGV.include?("--skip-server")
30
+ puts "\n== Starting development server =="
31
+ $stdout.flush # flush the output before exec(2) so that it displays
32
+ exec "bin/dev"
33
+ end
34
+ end