wallflower 0.4.0 → 0.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0414c4dd964967e59689c5e2e2b7a4fbbf7a20377c0ec440672f1fac3f9c11c5
4
- data.tar.gz: c3a640201ed58af2b59f003203b95ade7fcee6394e67b00cf944503b1498e49c
3
+ metadata.gz: bee05b46afa4694efc995d6ab7befa843e299fe7badc2cbb3525a81c243bc585
4
+ data.tar.gz: 27e1317e521c02f6fe23577cea0ce20e32351e187b79882994d75c774ce56e3e
5
5
  SHA512:
6
- metadata.gz: 481d9d8eee850f1b102268ff479d656424491030c32790eb13483e82d4809045e2f6f4296ed0783d988363a00fdb4f011ab6c4e3c9108766fc047d25d592b4e0
7
- data.tar.gz: bc71c88ee2441cc6f002b8c6f044135c3b32fe648662f35be6660259bf4d8d303220fa9686f73d8ed7b95792a1809e8dae80a34022e31a974a5f29940164714e
6
+ metadata.gz: 2d356ca88a0600f1310e9997e05d79b74665cece49b6a09f40f228e0b670a46c8f11e216e605dcd7650c7a596178333237456fb2c64dde46b2feaaca8fdbc100
7
+ data.tar.gz: 917d6dd960077ff4ad47d478c429534e1d974a110fac430149108b0ac7b26a2332ed97b59c862a372f572b0fa7162e621cbd8bff55c5b6e3584e286228c2aa65
data/README.md CHANGED
@@ -42,12 +42,12 @@ Wallflower.configure do |config|
42
42
  end
43
43
  ```
44
44
 
45
- ## Kinds and runners
45
+ ## Task types and runners
46
46
 
47
- A kind is a type of task the app offers, such as an export. Register each one in an initializer with its key, its title and the name of its runner class:
47
+ A task type is a type of task the app offers, such as an export. Register each one in an initializer with its key, its title and the name of its runner class:
48
48
 
49
49
  ```ruby
50
- Wallflower.register_kind :export_transactions, title: "Export transactions", runner: "ExportTransactionsRunner"
50
+ Wallflower.register_task_type :export_transactions, title: "Export transactions", runner: "ExportTransactionsRunner"
51
51
  ```
52
52
 
53
53
  An app whose registered runner class does not exist fails to boot, with a message naming the class.
@@ -91,7 +91,7 @@ A finished task's page then offers a Download button. The file is sent through W
91
91
  ## Starting a task
92
92
 
93
93
  ```ruby
94
- task = Wallflower.start(kind: :export_transactions, person: current_user, account: current_account, params: { "month" => "2026-10" })
94
+ task = Wallflower.start(task_type: :export_transactions, person: current_user, account: current_account, params: { "month" => "2026-10" })
95
95
  ```
96
96
 
97
97
  The task is returned queued and its job is enqueued. When the job runs, the task is marked running, its runner is called, and when the runner returns the task is marked finished with the time it finished.
@@ -110,7 +110,15 @@ With no `on_finish` set, tasks still run to the end.
110
110
 
111
111
  ## When a task fails
112
112
 
113
- A runner that raises marks its task failed. The task keeps the error's message, its page shows it beside how far the task got, the error is reported through `Rails.error`, and `on_finish` runs once with the failed task.
113
+ A task type runs one try by default. A task type registered with `tries:` runs again after a raised error until it has used them all, waiting `retry_wait:` seconds between tries, 60 by default:
114
+
115
+ ```ruby
116
+ Wallflower.register_task_type :send_price, title: "Send a price", runner: "SendPriceRunner", tries: 3, retry_wait: 60
117
+ ```
118
+
119
+ Between tries the task stays running and `on_finish` does not run. Each try starts its progress and refusals over, so a runner with more than one try must be safe to run again.
120
+
121
+ A runner that raises on its last try marks its task failed. The task keeps the error's message, its page shows it beside how far the task got, the error is reported through `Rails.error`, and `on_finish` runs once with the failed task.
114
122
 
115
123
  A task still running with no update for longer than `stall_after`, one hour by default, is marked failed by `Wallflower::SweepJob`, with a reason saying it stalled. Schedule the sweep the way the app schedules any recurring job, for example in Solid Queue's `config/recurring.yml`:
116
124
 
@@ -132,11 +140,11 @@ With `keep_for` set, the same sweep deletes each finished or failed task that en
132
140
 
133
141
  ## The task list
134
142
 
135
- The engine's root lists the tasks the signed-in person started in the current account, newest first. Each row shows the kind's title, the status as a coloured badge, the progress, the time left while it runs and when it was started, links to the task's page, and updates while the task runs.
143
+ The engine's root lists the tasks the signed-in person started in the current account, newest first. Each row shows the task type's title, the status as a coloured badge, the progress, the time left while it runs and when it was started, links to the task's page, and updates while the task runs.
136
144
 
137
145
  ## The task page
138
146
 
139
- A task's page lives at `tasks/:id` under the engine's mount path. The person who started the task sees its kind's title, its status and, once the runner has set a total, a progress bar of done against total. While the task runs and is at least five percent done, an estimate of the time left sits under the bar, worked out from how fast the task has gone since it started running. The page listens for changes to its task over Turbo Streams, so its status, progress and time left update without a reload, in a browser and in the Hotwire Native iOS app alike. Anyone else, and the same person viewing from another account, gets a not-found response.
147
+ A task's page lives at `tasks/:id` under the engine's mount path. The person who started the task sees its task type's title, its status and, once the runner has set a total, a progress bar of done against total. While the task runs and is at least five percent done, an estimate of the time left sits under the bar, worked out from how fast the task has gone since it started running. The page listens for changes to its task over Turbo Streams, so its status, progress and time left update without a reload, in a browser and in the Hotwire Native iOS app alike. Anyone else, and the same person viewing from another account, gets a not-found response.
140
148
 
141
149
  The task page and the task list reload in place when they are shown again after being hidden, such as when the iOS app returns from the background, so they show changes broadcast while they could not receive them. A page that stays visible makes no extra request. The copied controller is registered by the host's own Stimulus setup, as `wallflower--catch-up`.
142
150
 
@@ -4,16 +4,19 @@ module Wallflower
4
4
  class RunJob < ActiveJob::Base
5
5
  def perform(task)
6
6
  run(task)
7
- Wallflower.configuration.on_finish&.call(task)
7
+ Wallflower.configuration.on_finish&.call(task) if task.status.in?(%w[finished failed])
8
8
  end
9
9
 
10
10
  private
11
11
 
12
12
  def run(task)
13
- task.update!(status: "running", started_at: Time.current)
14
- Wallflower.kind(task.kind).runner.constantize.new.call(task)
13
+ task.refusals.destroy_all
14
+ task.update!(status: "running", started_at: Time.current, done: 0)
15
+ Wallflower.task_type(task.task_type).runner.constantize.new.call(task)
15
16
  task.update!(status: "finished", finished_at: Time.current)
16
17
  rescue StandardError => error
18
+ return retry_job(wait: Wallflower.task_type(task.task_type).retry_wait) if executions < Wallflower.task_type(task.task_type).tries
19
+
17
20
  task.update!(status: "failed", error_message: error.message, finished_at: Time.current)
18
21
  Rails.error.report(error, handled: true, context: { wallflower_task_id: task.id })
19
22
  end
@@ -20,8 +20,8 @@ module Wallflower
20
20
  STATUS_VARIANTS.fetch(status, :neutral)
21
21
  end
22
22
 
23
- def kind_title
24
- Wallflower.kind(kind).title
23
+ def task_type_title
24
+ Wallflower.task_type(task_type).title
25
25
  end
26
26
 
27
27
  def attach_result(io:, filename:)
@@ -4,7 +4,7 @@
4
4
  <% @tasks.each do |task| %>
5
5
  <div data-task-row>
6
6
  <%= turbo_stream_from task %>
7
- <%= link_to task.kind_title, task_path(task) %>
7
+ <%= link_to task.task_type_title, task_path(task) %>
8
8
  <%= render "wallflower/tasks/live", task: task %>
9
9
  <p>Started <%= task.created_at.strftime("%b %-d, %Y") %></p>
10
10
  </div>
@@ -1,7 +1,7 @@
1
1
  <%= turbo_stream_from @task %>
2
2
  <%= tag.div(data: { controller: "wallflower--catch-up" }) %>
3
3
  <%= ui_page(max_width: :md) do %>
4
- <h1><%= @task.kind_title %></h1>
4
+ <h1><%= @task.task_type_title %></h1>
5
5
  <%= render "wallflower/tasks/live", task: @task %>
6
6
  <% if @task.status == "finished" %>
7
7
  <p id="<%= dom_id(@task, :outcome) %>"><%= @task.changed_count %> changed, <%= @task.refusals.size %> refused</p>
@@ -1,7 +1,7 @@
1
1
  class CreateWallflowerTasks < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  create_table :wallflower_tasks do |t|
4
- t.string :kind, null: false
4
+ t.string :task_type, null: false
5
5
  t.string :status, null: false, default: "queued"
6
6
  t.json :params, null: false, default: {}
7
7
  t.integer :total
@@ -0,0 +1,5 @@
1
+ class RenameWallflowerTasksKindToTaskType < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ rename_column :wallflower_tasks, :kind, :task_type if column_exists?(:wallflower_tasks, :kind)
4
+ end
5
+ end
@@ -28,6 +28,10 @@ module Wallflower
28
28
  migration_template "add_started_at_to_wallflower_tasks.rb.erb", "db/migrate/add_started_at_to_wallflower_tasks.rb"
29
29
  end
30
30
 
31
+ def copy_task_type_migration
32
+ migration_template "rename_wallflower_tasks_kind_to_task_type.rb.erb", "db/migrate/rename_wallflower_tasks_kind_to_task_type.rb"
33
+ end
34
+
31
35
  def copy_catch_up_controller
32
36
  source = File.expand_path("../../../../app/javascript/wallflower/catch_up_controller.js", __dir__)
33
37
  create_file "app/javascript/controllers/wallflower/catch_up_controller.js", File.read(source)
@@ -4,6 +4,6 @@ module Wallflower
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace Wallflower
6
6
 
7
- config.after_initialize { Wallflower.check_kinds! }
7
+ config.after_initialize { Wallflower.check_task_types! }
8
8
  end
9
9
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wallflower
4
+ TaskType = Struct.new(:key, :title, :runner, :tries, :retry_wait, keyword_init: true)
5
+
6
+ class MissingRunner < StandardError; end
7
+
8
+ def self.register_task_type(key, title:, runner:, tries: 1, retry_wait: 60)
9
+ task_types[key.to_sym] = TaskType.new(key: key.to_sym, title: title, runner: runner.to_s, tries: tries, retry_wait: retry_wait)
10
+ end
11
+
12
+ def self.task_type(key)
13
+ task_types.fetch(key.to_sym)
14
+ end
15
+
16
+ def self.task_types
17
+ @task_types ||= {}
18
+ end
19
+
20
+ def self.check_task_types!
21
+ task_types.each_value do |task_type|
22
+ next if task_type.runner.safe_constantize
23
+
24
+ raise MissingRunner, "Wallflower task_type #{task_type.key} names the runner #{task_type.runner}, which does not exist."
25
+ end
26
+ end
27
+
28
+ def self.reset_task_types!
29
+ @task_types = {}
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wallflower
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/wallflower.rb CHANGED
@@ -2,11 +2,11 @@ require "keystone_ui"
2
2
  require "turbo-rails"
3
3
  require "wallflower/version"
4
4
  require "wallflower/configuration"
5
- require "wallflower/kinds"
5
+ require "wallflower/task_types"
6
6
  require "wallflower/engine"
7
7
 
8
8
  module Wallflower
9
- def self.start(kind:, person:, account: nil, params: {})
10
- Task.create!(kind: kind.to_s, person: person, account: account, params: params).tap { |task| RunJob.perform_later(task) }
9
+ def self.start(task_type:, person:, account: nil, params: {})
10
+ Task.create!(task_type: task_type.to_s, person: person, account: account, params: params).tap { |task| RunJob.perform_later(task) }
11
11
  end
12
12
  end
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: wallflower-develop
3
- description: Use PROACTIVELY for adding long-running work that a person starts and follows in a Rails app, such as an export or a report, including registering the kind of task, writing its runner, starting it, reporting progress, attaching a result file, linking a result page and refusing rows — MUST BE USED instead of hand-rolling a background job with its own status column, progress tracking, status page or file download.
3
+ description: Use PROACTIVELY for adding long-running work that a person starts and follows in a Rails app, such as an export or a report, including registering the task type, writing its runner, starting it, reporting progress, attaching a result file, linking a result page and refusing rows — MUST BE USED instead of hand-rolling a background job with its own status column, progress tracking, status page or file download.
4
4
  tools: Bash, Read, Write, Edit, Grep
5
5
  scope: background tasks — starting long-running work, following its status and progress, and getting its result
6
6
  ---
7
7
 
8
- This local implements background tasks with Wallflower. It always registers a kind, writes the kind's runner, and starts the task through `Wallflower.start`, asking the developer each decision listed below before writing code.
8
+ This local implements background tasks with Wallflower. It always registers a task type, writes the task type's runner, and starts the task through `Wallflower.start`, asking the developer each decision listed below before writing code.
9
9
 
10
10
  ## What wallflower is
11
11
 
@@ -13,8 +13,8 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
13
13
 
14
14
  ## Interface
15
15
 
16
- - `Wallflower.register_kind` — `Wallflower.register_kind(key, title:, runner:)` registers a type of task, with its key, the title people see, and the name of its runner class.
17
- - `Wallflower.start` — `Wallflower.start(kind:, person:, account: nil, params: {})` creates a queued task, enqueues its job, and returns the task.
16
+ - `Wallflower.register_task_type` — `Wallflower.register_task_type(key, title:, runner:, tries: 1, retry_wait: 60)` registers a type of task, with its key, the title people see, the name of its runner class, how many tries it gets and the seconds between them.
17
+ - `Wallflower.start` — `Wallflower.start(task_type:, person:, account: nil, params: {})` creates a queued task, enqueues its job, and returns the task.
18
18
  - `set_total` — `task.set_total(total)` saves the number of rows the task will work through.
19
19
  - `advance` — `task.advance(by = 1)` adds to the rows done, saving at most once a second.
20
20
  - `attach_result` — `task.attach_result(io:, filename:)` attaches a file as the task's result.
@@ -24,17 +24,17 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
24
24
  ## How to use it
25
25
 
26
26
  1. Ask the developer these before writing code:
27
- - What is the kind's key, its title, and its runner's class name?
27
+ - What is the task type's key, its title, and its runner's class name?
28
28
  - Who is the person who starts the task, and does it belong to an account?
29
29
  - What are the task's params?
30
30
  - Does the task work through rows, and can a row be refused?
31
31
  - Is the result a file, a page in the app, both, or nothing?
32
32
  - Where does the person go after starting the task?
33
33
 
34
- 2. Register the kind in an initializer:
34
+ 2. Register the task type in an initializer:
35
35
 
36
36
  ```ruby
37
- Wallflower.register_kind :export_transactions, title: "Export transactions", runner: "ExportTransactionsRunner"
37
+ Wallflower.register_task_type :export_transactions, title: "Export transactions", runner: "ExportTransactionsRunner"
38
38
  ```
39
39
 
40
40
  3. Write the runner as a class in the app. Wallflower calls `new` on it with no arguments, then `call` with the task. The app fails to boot, with a message naming the class, if the registered class does not exist.
@@ -67,10 +67,10 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
67
67
  6. Start the task from the app's own code, passing params as a hash with string keys, since they are stored in a JSON column:
68
68
 
69
69
  ```ruby
70
- task = Wallflower.start(kind: :export_transactions, person: current_user, account: current_account, params: { "month" => "2026-10" })
70
+ task = Wallflower.start(task_type: :export_transactions, person: current_user, account: current_account, params: { "month" => "2026-10" })
71
71
  ```
72
72
 
73
- `person` is required and `account` is optional. `start` does not check that the kind is registered. The runner runs when the job runs, not during this call.
73
+ `person` is required and `account` is optional. `start` does not check that the task type is registered. The runner runs when the job runs, not during this call.
74
74
 
75
75
  7. Send the person to the task. Its page is at `tasks/:id` under the path where the engine is mounted, and the list of tasks is at the root of that path.
76
76
 
@@ -79,6 +79,7 @@ Wallflower tracks long-running work in a Rails app: a person starts a task, foll
79
79
  - Pass the same person the app treats as the current person, since the task list, the task page and the download answer only that person.
80
80
  - When the app has a current account configured, pass that same account, since those pages then show only tasks in that account and a task started with another account or none does not appear.
81
81
  - The job sets the task's status, so a runner never sets it. The task is marked running before the runner is called and finished when the runner returns.
82
- - To fail a task, let a `StandardError` leave the runner. The task is marked failed, its message is stored and shown to the person on the task's page, and the error is reported through `Rails.error`. Write that message for the person reading it, and do not rescue an error the person needs to see, since a runner that returns marks the task finished.
82
+ - A task type with more than one try runs its runner again after a `StandardError`. Between tries the task stays running and each try starts its progress and refusals over, so that runner must be safe to run again.
83
+ - To fail a task, let a `StandardError` leave the runner on its last try. The task is marked failed, its message is stored and shown to the person on the task's page, and the error is reported through `Rails.error`. Write that message for the person reading it, and do not rescue an error the person needs to see, since a runner that returns marks the task finished.
83
84
  - A running task with no update for longer than the app's stall time, one hour by default, can be marked failed as stalled. Call `advance` or `set_total` often enough that a slow step does not reach that time.
84
85
  - Installing the gem, setting up its pages, its configuration, code that runs when a task finishes, and scheduling stalled-task checks and old-task deletion are out of scope. They belong to the install local.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wallflower-info
3
- description: Use to learn what wallflower offers — background tasks, kinds and runners, task statuses and progress, results, and the task pages.
3
+ description: Use to learn what wallflower offers — background tasks, task types and runners, task statuses and progress, results, and the task pages.
4
4
  tools: Read
5
5
  scope: background tasks — starting long-running work, following its status and progress, and getting its result
6
6
  ---
@@ -15,17 +15,17 @@ Reach for it when an app has work that runs in a background job and the person w
15
15
 
16
16
  ## Interface
17
17
 
18
- This local documents no commands, because wallflower declares none for it. The install local owns adding the gem to an app: its generators, mounting its engine, its settings and scheduling its recurring sweep. The develop local owns building on it: registering kinds, writing runners, starting tasks, and reporting progress and results from a runner.
18
+ This local documents no commands, because wallflower declares none for it. The install local owns adding the gem to an app: its generators, mounting its engine, its settings and scheduling its recurring sweep. The develop local owns building on it: registering task types, writing runners, starting tasks, and reporting progress and results from a runner.
19
19
 
20
20
  ## How to use it
21
21
 
22
22
  - To hook wallflower into an app or update it there, use `wallflower-install`.
23
- - To add a kind of task or change what a task does, use `wallflower-develop`.
23
+ - To add a task type or change what a task does, use `wallflower-develop`.
24
24
 
25
25
  ## Conventions
26
26
 
27
27
  - **Task**: one run of long-running work, stored with the params, person and account it was started with.
28
- - **Kind**: a type of task the app offers, such as an export, with a key, a title and the name of its runner class.
28
+ - **Task type**: a type of task the app offers, such as an export, with a key, a title and the name of its runner class.
29
29
  - **Runner**: a plain class whose `call` receives the task and does the work.
30
30
  - **Person**: the record that started a task. Only that person can see the task.
31
31
  - **Account**: the optional record a task belongs to. When the app names a current account, a person sees only the tasks in that account.
@@ -8,7 +8,7 @@ install:
8
8
  - Wallflower::SweepJob
9
9
 
10
10
  develop:
11
- - Wallflower.register_kind
11
+ - Wallflower.register_task_type
12
12
  - Wallflower.start
13
13
  - set_total
14
14
  - advance
@@ -19,7 +19,7 @@ develop:
19
19
  sources:
20
20
  - lib/wallflower.rb
21
21
  - lib/wallflower/configuration.rb
22
- - lib/wallflower/kinds.rb
22
+ - lib/wallflower/task_types.rb
23
23
  - lib/wallflower/engine.rb
24
24
  - app/models/wallflower/task.rb
25
25
  - app/models/wallflower/refusal.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wallflower
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -109,11 +109,12 @@ files:
109
109
  - lib/generators/wallflower/install/templates/add_started_at_to_wallflower_tasks.rb.erb
110
110
  - lib/generators/wallflower/install/templates/create_wallflower_refusals.rb.erb
111
111
  - lib/generators/wallflower/install/templates/create_wallflower_tasks.rb.erb
112
+ - lib/generators/wallflower/install/templates/rename_wallflower_tasks_kind_to_task_type.rb.erb
112
113
  - lib/generators/wallflower/update/update_generator.rb
113
114
  - lib/wallflower.rb
114
115
  - lib/wallflower/configuration.rb
115
116
  - lib/wallflower/engine.rb
116
- - lib/wallflower/kinds.rb
117
+ - lib/wallflower/task_types.rb
117
118
  - lib/wallflower/version.rb
118
119
  - the_local/agents/wallflower-develop.md
119
120
  - the_local/agents/wallflower-info.md
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Wallflower
4
- Kind = Struct.new(:key, :title, :runner, keyword_init: true)
5
-
6
- class MissingRunner < StandardError; end
7
-
8
- def self.register_kind(key, title:, runner:)
9
- kinds[key.to_sym] = Kind.new(key: key.to_sym, title: title, runner: runner.to_s)
10
- end
11
-
12
- def self.kind(key)
13
- kinds.fetch(key.to_sym)
14
- end
15
-
16
- def self.kinds
17
- @kinds ||= {}
18
- end
19
-
20
- def self.check_kinds!
21
- kinds.each_value do |kind|
22
- next if kind.runner.safe_constantize
23
-
24
- raise MissingRunner, "Wallflower kind #{kind.key} names the runner #{kind.runner}, which does not exist."
25
- end
26
- end
27
-
28
- def self.reset_kinds!
29
- @kinds = {}
30
- end
31
- end