Guide
Optimistic locking
One line on a model, and a stale save is refused instead of silently overwriting.
The problem
Two people open the same record, both save. The second write overwrites the first, and neither is told. Guarding against that in every endpoint means remembering to, every time, forever.
The whole feature
import {
ParseClass, BaseModel, ParseVersionField, createVersionedMongoAdapter,
VersionRegistry,
} from 'parse-server-kit';
@ParseClass('Job')
class Job extends BaseModel {
@ParseVersionField() // declares the Number field itself
declare version: number;
}
That is all of it. No endpoint reads or writes the field:
- every object read carries the version it was read at, and every
save()andsaveAll()asserts it; - the adapter moves the assertion into the write's filter — the update only lands if the row is still at that version — and increments the field, so the next reader gets a fresh number;
- a save that lost the race is refused with
CONFLICT; a genuinely missing row still reads as missing; - creates get version
1from the adapter, so callers never supply one.
An object built from a bare id is not protected
Model.pointer(id) was never read, so it carries no version and has nothing
to assert. That is by design — there is no safe value to guess — but it means a
read-modify-write must actually read.
Requirements
Needs createVersionedMongoAdapter, the same adapter that powers transactions. It is MongoDB only; on Postgres the field is declared but nothing enforces it — and since 3.0 that says so at boot rather than passing unnoticed.
// In the boot output:
[Versioning] Optimistic locking active on: Job, Invoice
// Or, if the adapter was never wired in:
[Versioning] 2 class(es) declare @ParseVersionField … but
createVersionedMongoAdapter() was never called. Optimistic locking
is INACTIVE - stale saves will overwrite silently.
Recovering
try {
await job.save(null, { useMasterKey: true });
} catch (error) {
if (error instanceof Parse.Error && error.code === CONFLICT) {
const fresh = await new Parse.Query(Job).get(job.id);
// re-apply the change to `fresh` and save again
}
throw error;
}
VERSION_CONFLICT and CONFLICT are the same value deliberately, and CONFLICT_MESSAGE is already phrased for an end user.
Inspecting it
VersionRegistry.classNames(); // every versioned class
VersionRegistry.isVersioned('Job'); // true
VersionRegistry.adapterIsInstalled();