/** * A write refused because the record is no longer the version it was made against (412), or because the page * named no version at all (428, an old page). Nothing was saved either way; the user's choice is still theirs. */ export class RecordChangedError extends Error { // A protected member makes the class nominal: no other error, whatever its fields, passes as this one. protected readonly ownedNotice!: true; constructor( message: string, /** The page is too old to name a version: reload, rather than review and save again. */ public readonly outOfDate = false, /** The record as it is now, when the refusal knows it. */ public readonly current?: unknown, ) { super(message); this.name = 'RecordChangedError'; } } export const isRecordChanged = (error: unknown): error is RecordChangedError => error instanceof RecordChangedError || (error as { name?: unknown } | null)?.name === 'RecordChangedError'; // The standard refusal: nothing is merged or retried over another person's change. The person reloads the record // (which discards their unsaved edit, and is said before they do) or leaves. export const RECORD_CHANGED_MESSAGE = 'This record changed elsewhere. Your changes were not saved. Reload the record before editing again.'; export const PAGE_OUT_OF_DATE_MESSAGE = 'This page is out of date. Reload before saving.'; export const RELOAD_DISCARDS_MESSAGE = 'Reload record discards the changes you have not saved.'; /** The precondition a conditional write names: the version it was built on. */ export const ifMatch = (version: string): Record => ({ 'If-Match': `W/"${version}"` }); /** A refused conditional write, in the words above; anything else passes through. */ export const asRecordChanged = (error: unknown): unknown => { const status = (error as { status?: unknown } | null)?.status; if (status === 412) return new RecordChangedError(RECORD_CHANGED_MESSAGE); if (status === 428) return new RecordChangedError(PAGE_OUT_OF_DATE_MESSAGE, true); return error; };