Backbone.Model save() idempotency and request duplication limits
0 reputation · 19 Sept 2023, 17:36 UTC
0 reputation · 19 Sept 2023, 17:36 UTC
Backbone.js models use the save() method to synchronize client-side attributes with a remote data store. While the framework automatically selects between POST and PUT based on the model's existence, it does not natively track pending requests or implement a retry mechanism for network timeouts.
In scenarios where a network request hangs or a timeout occurs, triggering a subsequent save() call on the same model instance can result in duplicate writes to the server if the initial request eventually completes. This behavior places the responsibility of idempotency entirely on the server-side implementation or the developer's event handling logic.
Given that Model.save() does not provide a built-in way to cancel pending requests or ensure a single flight of synchronization:
sync and error events?save() operation to prevent concurrent requests on a single model?31775 reputation · 20 Sept 2023, 04:59 UTC
Backbone’s save() simply fires an AJAX request each time it is invoked. It does not keep a record of pending requests, nor does it automatically cancel or deduplicate concurrent calls. Therefore, two rapid save() invocations can result in two identical network requests, which may create duplicate records on the server if the first request eventually succeeds.
POST for new models (no id), PUT for existing models (has id).isSyncing or a request queue.To guard against duplicate writes when you implement retry logic, add a lightweight flag or queue on the model instance. The pattern is:
model.isSyncing = true;sync (or wrap the call) to check the flag: if model.isSyncing is true, either queue the request or simply return a resolved Promise that does nothing.success and error callbacks: model.isSyncing = false;model.isSyncing is false: this guarantees that at most one request is in flight.Example wrapper:
var syncWithGuard = function(method, model, options) {
if (model.isSyncing) {
// Optionally queue or ignore
return;
}
model.isSyncing = true;
var originalSuccess = options.success;
var originalError = options.error;
options.success = function(resp) {
model.isSyncing = false;
if (originalSuccess) originalSuccess(resp);
};
options.error = function(err) {
model.isSyncing = false;
if (originalError) originalError(err);
};
return Backbone.sync(method, model, options);
};
Backbone.sync = syncWithGuard; // Install globally or per‑model
With this guard, a second save() while the first is still pending will simply return without firing another request, preventing duplicate writes.
id, Backbone will use PUT. According to the HTTP spec, PUT is idempotent; repeating the same PUT will not create duplicate resources.PUT semantics (e.g., ignoring duplicate payloads or using optimistic locking), you can rely on the HTTP layer to avoid duplication.id, POST is non‑idempotent, so the client‑side guard above is essential.model.save() twice in quick succession.POST if no id, PUT if id is present.To fine‑tune the recommendation, it would help to know whether your model has an id set before the first save() call. If it does, you can rely on PUT idempotency; if not, the client‑side guard becomes mandatory.
Use comments to ask for clarification. Post a solution as an answer.
31,775 reputation · 20 Sept 2023, 01:59 UTC
Backbone.Model.save() returns the jqXHR object from the underlying Ajax call, so you can store that reference and abort it before issuing a new save. By listening to the model’s request and sync events you can set a temporary flag (e.g., this._saving) that is cleared on success or error, guaranteeing only one request is in flight at a time. If you prefer not to abort, rely on server‑side idempotency (PUT for updates) and use the {wait:true} option to keep client attributes unchanged until the server confirms the change.