Snapshots
Frozen definitions, response status, and compare-and-swap drafts.
One of the foundational invariants of dimah-form is Snapshot Immutability: when a fill session begins, the system takes an exact deep snapshot of the form schema and stores it directly inside response.definition.
The Snapshot Invariant: All draft patching, conditional visibility
calculations (showWhen), and final submission validations execute against
response.definition, never against the live form schema.
Why Snapshots Matter
In real-world applications, form schemas are constantly evolving. An administrator might:
- Add a new required field.
- Remove an existing field.
- Modify option values in a select dropdown.
- Change conditional visibility rules.
Without snapshots, an in-progress draft started on Monday would fail validation on Wednesday because the live form schema changed mid-fill.
With dimah-form:
- Active responses stay completely isolated from live schema edits.
- In-flight drafts can always be completed and submitted cleanly.
- Historical submitted responses preserve the exact questions and options that the respondent saw when they filled the form.
Response Statuses & State Machine
A response record transitions through three possible statuses:
stateDiagram-v2
[*] --> draft: startResponse()
draft --> draft: saveDraft()
draft --> submitted: submitResponse()
draft --> abandoned: abandonResponse()
submitted --> draft: reopenResponse()
abandoned --> draft: reopenResponse()
submitted --> [*]: deleteResponse()
abandoned --> [*]: deleteResponse()| Status | Meaning | Allowed API Actions |
|---|---|---|
draft | In-progress fill session. Required fields can be empty. | saveDraft, submitResponse, abandonResponse |
submitted | Finalized and locked. All visible required fields were validated. | reopenResponse, deleteResponse, getResponse |
abandoned | Closed by user or admin without submitting. Locked from edits. | reopenResponse, deleteResponse, getResponse |
Lifecycle Methods
- 1. StartFreeze definition snapshot
- 2. DraftPartial answer patches (null deletes)
- 3. SubmitValidate visible fields vs snapshot
- 4. ReopenUnlock row back to draft
1. startResponse
- Resolves the target form by
idorslug. - Verifies that the form has
status: "active". - Freezes the form definition into
response.definition. - Seeds any configured
defaultValues intoresponse.answers. - If called with
{ resume: true, respondentId: "user-123" }, it checks for an existing unfinished draft and returns it instead of creating a duplicate.
2. saveDraft
- Accepts partial answer patches.
- Passing
nullas an answer value deletes that key fromanswers. - Does not require visible required fields to be filled, enabling incremental progress across multi-page forms.
- Verifies that the record status is currently
draft.
3. submitResponse
- Accepts final answer updates.
- Evaluates all conditional
showWhenvisibility rules against the current answers. - Strips any hidden fields from the persisted answers so stale answers do not pollute your database.
- Validates that every visible field satisfies its validation rules and
requiredconstraints. - Sets
status: "submitted"and recordssubmittedAt: new Date().toISOString().
4. abandonResponse
- Sets
status: "abandoned". - Locks the response row from further draft edits.
5. reopenResponse
- Moves a
submittedorabandonedresponse back todraft. - Preserves the original definition snapshot and answer values, allowing the respondent to edit and re-submit.
Optimistic Concurrency Control (CAS)
When multiple tabs are open or when network connections reconnect, concurrent writes can accidentally overwrite newer answers.
dimah-form implements Compare-And-Swap (CAS) concurrency control using the updatedAt timestamp:
- When the client loads or saves a draft, it receives the record's current
updatedAtISO timestamp. - Subsequent
saveDraftorsubmitResponserequests includeupdatedAt. - The database adapter compares
expectedUpdatedAtagainst the stored row:- If they match, the update commits and a new
updatedAttimestamp is generated. - If they differ (because another tab or request wrote to the row first), the server rejects the request with a
409 STALE_UPDATEerror.
- If they match, the update commits and a new
- The
useFormResponsehook automatically catchesSTALE_UPDATE, refreshes the latest server state, and notifies your UI.
The Response Record Schema
Every response record in your database conforms to the following TypeScript structure:
type ResponseRecord = {
/** Unique response identifier (e.g., CUID or UUID) */
id: string;
/** Foreign key to the parent form */
formId: string;
/** Current lifecycle status */
status: "draft" | "submitted" | "abandoned";
/** Frozen snapshot of the form definition at start time */
definition: FormSnapshot;
/** Key-value dictionary of respondent answers */
answers: Record<string, unknown>;
/** Optional user or session identifier */
respondentId: string | null;
/** Timestamp when the response was finalized */
submittedAt: string | null;
/** Record creation timestamp */
createdAt: string;
/** Last update timestamp (used for CAS optimistic locking) */
updatedAt: string;
};