mutateOrSkip

fun mutateOrSkip(transform: (S) -> Patch<S>?): Boolean

mutate, for a transform that may decline: read the state, decide, and maybe write — still as one atomic step. Returns whether a patch was published.

null from transform means "on this state, write nothing". It is a decision, not a missing argument: the refusal is an outcome of the read-modify-write, made against the very state the patch would have landed on, and it is the reason this overload exists.

// Claim the slot only if nobody holds it — atomically, and silently if somebody does.
val claimed = board.mutateOrSkip { seats ->
if (seats.get(seat) != null) null else Patch(seats.set(replica, clock, seat, me))
}

Why not just return an identity patch. A patch that joins to nothing does leave state unchanged — but apply does not know that. It burns a sequence number, buffers the delta until every peer acks it, and broadcasts an empty QuiltMessage.Delta to the whole room. On a refusal that fires often, that is a frame per refusal for no information. The alternative — testing the condition before the lock and returning early — is cheaper still but answers a different question: it decides against a state nothing is holding still, so by the time the call returns another writer may have made the answer wrong. mutateOrSkip is the one that is both: the decision is inside the lock, and a refusal costs nothing.

The return is a plain Booleandid this publish? — because that is the whole of what a caller cannot otherwise recover: transform runs inside the critical section, so its own result is not visible outside without a captured var. A caller that needs to know what it published already holds it, in the branch that built the patch.

Same contract as mutate otherwise: transform runs inside the locked section, so it must be pure, fast, and non-suspending, and it may run against a state newer than any the caller has read.

A closed replicator throws on both branches, the declining one included, so false never has two meanings. That is deliberate and it is reachable: a Quilter closes itself when its Seam completes, so a consumer that polls a conditional write on a tick — a scheduler round that usually has nothing to do, a presence declaration that is usually already current — sees a throw at teardown where an unconditional mutate would have thrown too. The alternative, reporting a dead replicator as a refusal, silently converts "this session is over" into "the state said no", which is the answer a caller acts on.

Return

true if transform returned a patch and it was applied and broadcast; false if transform declined, in which case nothing changed and no frame was sent.

Throws

if this replicator has been closed — checked before transform runs, so it fires on the declining path as well as the publishing one.

Samples

runTest(
    StandardTestDispatcher(),
    timeout = TEST_WEDGE_BACKSTOP,
) {
    val loom = InMemoryLoom()
    val seam = loom.host(Pattern("seat-claims"))

    val seats = Quilter(
        seam,
        LWWMap.empty<String, String>(),
        LWWMap.serializer(serializer<String>(), serializer<String>()),
        backgroundScope,
        config = QuilterConfig(expectVirtualTime = true),
    )

    // Claim a seat only if it is still free. Read and write are one atomic step, so no other
    // writer can take the seat between the check and the publish.
    fun claim(seat: String, player: String, at: Long): Boolean =
        seats.mutateOrSkip { board ->
            if (board[seat] != null) null else board.set(seats.replica, at, seat, player)
        }

    assertEquals(true, claim("north", "alice", 1L), "the seat was free — published")
    assertEquals(false, claim("north", "bob", 2L), "already taken — nothing published, no frame sent")
    assertEquals("alice", seats.state.value["north"])
}