Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/javascript/controllers/auto_save_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const AUTOSAVE_INTERVAL = 3000

export default class extends Controller {
#timer
#savePromise

// Lifecycle

Expand All @@ -17,6 +18,8 @@ export default class extends Controller {
async submit() {
if (this.#dirty) {
await this.#save()
} else if (this.#savePromise) {
await this.#savePromise
}
}

Expand All @@ -34,7 +37,12 @@ export default class extends Controller {

async #save() {
this.#resetTimer()
await submitForm(this.element)
this.#savePromise = submitForm(this.element)
try {
await this.#savePromise
} finally {
this.#savePromise = null
Comment on lines +40 to +44
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#save() overwrites #savePromise unconditionally. If a save is already in-flight (high latency) and another save is triggered (e.g., via change scheduling a new timer and then submit() calling #save()), multiple concurrent PATCHes can be sent and #savePromise will only track the most recent one. That means callers awaiting submit() may return while an earlier request is still in-flight, and responses can apply out of order, potentially persisting stale data last. Consider serializing saves (await/chain any existing #savePromise before starting a new submitForm) and avoid clearing a newer promise in the finally of an older save.

Suggested change
this.#savePromise = submitForm(this.element)
try {
await this.#savePromise
} finally {
this.#savePromise = null
const previousSavePromise = this.#savePromise || Promise.resolve()
const savePromise = previousSavePromise.then(() => submitForm(this.element))
this.#savePromise = savePromise
try {
await savePromise
} finally {
if (this.#savePromise === savePromise) {
this.#savePromise = null
}

Copilot uses AI. Check for mistakes.
}
}

#resetTimer() {
Expand Down
5 changes: 5 additions & 0 deletions app/javascript/controllers/clicker_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { nextFrame } from "helpers/timing_helpers";

export default class extends Controller {
static targets = [ "clickable" ]
static outlets = [ "auto-save" ]

async click() {
if (this.hasAutoSaveOutlet) {
await this.autoSaveOutlet.submit()
}

await nextFrame()
this.#clickable.click()
}
Expand Down
6 changes: 4 additions & 2 deletions app/views/cards/container/footer/_create.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
<%= button_to card_publish_path(card), name: "creation_type", value: "add", class: "btn",
title: "Create card (#{ hotkey_label(["ctrl", "enter"]) })",
form: { data: { controller: "form bridge--form" } },
data: { form_target: "submit", bridge__form_target: "submit", controller: "clicker", action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
data: { form_target: "submit", bridge__form_target: "submit", controller: "clicker", clicker_auto_save_outlet: "#card_form",
action: "keydown.ctrl+enter@document->clicker#click keydown.meta+enter@document->clicker#click" } do %>
<span>Create card</span>
<% end %>

<%= button_to card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
title: "Create and add another (#{ hotkey_label(["ctrl", "shift", "enter"]) })", form: { data: { controller: "form" } },
data: { form_target: "submit", controller: "clicker", action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
data: { form_target: "submit", controller: "clicker", clicker_auto_save_outlet: "#card_form",
action: "keydown.ctrl+shift+enter@document->clicker#click keydown.meta+shift+enter@document->clicker#click" } do %>
<span>Create and add another</span>
<% end %>
</div>
Expand Down
Loading