#2011·iii

Proposal: conditional single-key compare-and-set for state

Author: hoangtung4398Created Jul 24, 2026Updated Aug 18, 2026
Labelsstate

Summary

I would like maintainer guidance on adding a conditional single-key state operation to iii.

The operation would compare the complete currently stored JSON value for one scope and key with a caller-provided expected value and replace it with a new complete value only when they are structurally equal.

The primary goal is to prevent stale full-record overwrites across workers, processes, and machines sharing an authoritative state backend.

This issue requests alignment only. No implementation or pull request has been started.

Proposed semantics

Conceptual function:

state::compare-and-set

Conceptual input:

typescript
interface StateCompareAndSetInput<T = unknown> {
  scope: string;
  key: string;
  expected_value: T;
  new_value: T;
}

Conceptual normal result:

typescript
type StateCompareAndSetResult =
  | { outcome: "applied" }
  | { outcome: "conflict" }
  | { outcome: "not_found" };

Required behavior:

  • compare the complete persisted JSON value, not selected fields;
  • replace the value only when the complete expected value matches;
  • perform comparison and replacement at one same-key linearization point;
  • serialize against same-key set, update, delete, and other conditional writes;
  • work across workers and processes using the same authoritative backend;
  • return conflict without returning the current stored value;
  • return not_found without creating the key;
  • never emulate the operation using client-side get followed by set;
  • never silently fall back to existing state::update.

Object member ordering would not affect structural equality, while array order would remain significant.

Why existing state operations are insufficient

The inspected public state APIs provide get, set, delete, update, and listing operations.

Existing atomic ordered updates do not accept caller-provided expected state and do not return a distinct precondition-conflict result. A client-side get/set sequence cannot prevent a stale write from overwriting an intervening mutation.

Adapter correctness

The inspected state worker ships built-in KV/file, Redis, and bridge adapters.

My current conservative proposal is:

ALL_SHIPPED_ADAPTERS_MUST_SUPPORT

A configured adapter should not silently provide weaker semantics.

An alternative could be explicit capability discovery where unsupported adapters fail closed, but that would need a maintainer-approved capability contract.

Failure and retry behavior

The design distinguishes:

  • a definitive failure known not to have committed;
  • an outcome-unknown failure after the request may have reached the backend.

A timeout or response loss after dispatch must not automatically be reported as a definitive non-write.

Clients should not blindly retry an ambiguous conditional mutation. They should reread the key and reconcile whether the current value equals the expected value, the requested replacement, or a third value.

Current downstream context

One downstream application currently consumes:

[email protected]
iiidev/iii:0.11.2

The corresponding inspected release source is:

iii/v0.11.2^{}
2b445957701f94dc5f56f900af314e9d59f3b0f7

A fixed newer comparison snapshot was also inspected:

c84f918f6f5e92e32ad78e6695d581c9e1995c9b

Neither inspected public surface contains this conditional operation.

These are compatibility references only. I am not proposing a target branch without maintainer guidance.

Alignment requested

Could maintainers please advise on the following?

  1. Is this conditional state primitive acceptable in principle?
  2. Should a contribution target current main, a maintained release line, or another planned state API?
  3. Must every shipped state adapter implement the operation, or should unsupported adapters expose an explicit fail-closed capability result?
  4. Is state::compare-and-set an acceptable function name and are applied/conflict/not_found acceptable normal outcomes?
  5. Where should the request/result types live across the Rust and Node SDKs?
  6. Which engine, SDK, adapter, concurrency, crash, and backend integration tests would be required?
  7. How would an accepted change be released across the engine binary, Docker image, Rust SDK, and Node SDK?
  8. Would maintainers prefer a narrower design or a different concurrency primitive?

Proposed acceptance properties

A future implementation should demonstrate:

  • at most one successful writer when two writers use the same expected value;
  • no stale overwrite;
  • no partial replacement;
  • same-key ordering against set/update/delete;
  • conflict and missing-key behavior;
  • cross-process correctness where the backend supports multiple clients;
  • explicit behavior for timeout and response loss;
  • durability and read visibility after an acknowledged application;
  • no unsafe fallback for unsupported adapters.

Contribution boundary

I will not begin an implementation until the target line, adapter policy, public contract, test requirements, and expected release path have been aligned with maintainers.