amruthpillai/reactive-resume · warning · ORPCError

STYLESHEET_REVISION_CONFLICT

STYLESHEET_REVISION_CONFLICT

Error message

The resume or stylesheet changed while the candidate was being validated.

What it means

Optimistic-concurrency guard. The stylesheet service reads a snapshot, does compile/preflight/parity work outside the transaction, then re-locks the row (SELECT ... FOR UPDATE) inside the transaction and compares stylesheetRevision + renderDataVersion to the client's expected values. If either changed, it throws StylesheetRevisionConflict, surfaced as STYLESHEET_REVISION_CONFLICT (HTTP 409) with the fresh state in error.data.state.

Source

Thrown at packages/api/src/features/resume/stylesheet-service.ts:339

					const data = parseStoredResumeData({
						...locked.data,
						metadata: { ...locked.data.metadata, stylesheet: next },
					});
					return transaction.update({ snapshot: locked, data });
				});

				await dependencies.publish(updated);
				observeActivation(true, updated.stylesheetRevision);

				return {
					...(await stateFromSnapshot(updated, dependencies.convertLegacy)),
					editGeneration: input.editGeneration,
					diagnostics,
				};
			} catch (error) {
				if (error instanceof StylesheetRevisionConflict) {
					observeActivation(false, error.snapshot.stylesheetRevision);
					throw new ORPCError("STYLESHEET_REVISION_CONFLICT", {
						status: 409,
						message: error.message,
						data: {
							state: await stateFromSnapshot(error.snapshot, dependencies.convertLegacy),
						},
					});
				}

				observeActivation(false, snapshot.stylesheetRevision);
				throw error;
			}
		},
	};
}

type StylesheetDatabase = Pick<typeof db, "select" | "transaction">;

type DatabaseStylesheetServiceOptions = {

View on GitHub (pinned to 3a5b12e2a4)

Solutions

  1. Read error.data.state (the fresh snapshot) and re-issue activate with the new expectedRevision/expectedRenderDataVersion.
  2. Serialize stylesheet edits so only one is in flight per resume.
  3. Refresh the stylesheet state in the UI before retrying.

Example fix

// before
stylesheetService.activate({ id, userId, source, expectedRevision: cached.rev, expectedRenderDataVersion: cached.rdv, ... });
// after — handle 409 by refreshing
try { await activate(...); }
catch (e) {
  if (e.code === 'STYLESHEET_REVISION_CONFLICT') {
    cached = e.data.state; // fresh rev + rdv
    await activate({ ..., expectedRevision: cached.stylesheetRevision, expectedRenderDataVersion: cached.renderDataVersion });
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

function expectedMatches(snapshot, expected) {
  return snapshot.stylesheetRevision === expected.revision && snapshot.renderDataVersion === expected.renderDataVersion;
}

Try / catch

async function activateWithRefresh(input, getState) {
  for (;;) {
    try { return await stylesheetService.activate(input); }
    catch (e) {
      if (e.code !== 'STYLESHEET_REVISION_CONFLICT') throw e;
      const fresh = e.data.state;
      input.expectedRevision = fresh.stylesheetRevision;
      input.expectedRenderDataVersion = fresh.renderDataVersion;
    }
  }
}

Prevention

When it happens

Trigger: Two concurrent activations, or a resume data edit landing between reading the snapshot and the transactional lock, so the client's expectedRevision/expectedRenderDataVersion are stale.

Common situations: Two tabs or users editing the same resume, an AI agent and a human editing concurrently, or a client that held stale state for a long time before submitting.

Related errors


AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12). Data as JSON: /api/errors/ce8bd509bdcdd468. Report an issue: GitHub.