Proposal: conditional single-key compare-and-set for state
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-setConceptual input:
interface StateCompareAndSetInput<T = unknown> {
scope: string;
key: string;
expected_value: T;
new_value: T;
}Conceptual normal result:
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
conflictwithout returning the current stored value; - return
not_foundwithout creating the key; - never emulate the operation using client-side
getfollowed byset; - 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_SUPPORTA 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.2The corresponding inspected release source is:
iii/v0.11.2^{}
2b445957701f94dc5f56f900af314e9d59f3b0f7A fixed newer comparison snapshot was also inspected:
c84f918f6f5e92e32ad78e6695d581c9e1995c9bNeither 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?
- Is this conditional state primitive acceptable in principle?
- Should a contribution target current main, a maintained release line, or another planned state API?
- Must every shipped state adapter implement the operation, or should unsupported adapters expose an explicit fail-closed capability result?
- Is
state::compare-and-setan acceptable function name and areapplied/conflict/not_foundacceptable normal outcomes? - Where should the request/result types live across the Rust and Node SDKs?
- Which engine, SDK, adapter, concurrency, crash, and backend integration tests would be required?
- How would an accepted change be released across the engine binary, Docker image, Rust SDK, and Node SDK?
- 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.
Source: iii-hq/iii