Quilter

fun <S : Quilted<S>> Quilter(seam: Seam, initial: S, valueSerializer: KSerializer<S>, scope: CoroutineScope, replica: ReplicaId = us.tractat.kuilt.crdt.ReplicaId(seam.selfId.value), config: QuilterConfig = QuilterConfig(), clock: MonotonicMillis = SystemMonotonicMillis, binaryFormat: BinaryFormat = kotlinx.serialization.cbor.Cbor, deltaTargets: (Set<PeerId>) -> Set<PeerId> = { it }, random: Random = kotlin.random.Random.Default): Quilter<S>

Convenience factory for Quilter that derives the message serializer internally.

The full constructor requires callers to wrap the value serializer manually via QuiltMessage.serializer(valueSerializer). This factory does that wrapping for you, and defaults replica to ReplicaId(seam.selfId.value) — a safe default because each Seam has a unique us.tractat.kuilt.core.Seam.selfId, satisfying the one-instance-per- (replica, CRDT-type) precondition as long as each peer creates exactly one replicator per CRDT type. Override replica when you need a custom id (e.g. a stable persistent id that survives reconnects with a different peer identity).

val tally = Quilter(seam, PNCounter.ZERO, PNCounter.serializer(), backgroundScope)
tally.mutate { it.increment(tally.replica, 3L) }

Parameters

seam

the us.tractat.kuilt.core.Seam to replicate over.

initial

the starting CRDT state (typically the zero/empty value).

valueSerializer

the KSerializer for S. The message serializer is derived automatically.

scope

the CoroutineScope whose Job parents the replicator's owned child job.

replica

this peer's ReplicaId; defaults to ReplicaId(seam.selfId.value).

config

replication behaviour tuning.

clock

monotonic time source; override in tests.

binaryFormat

binary codec for wire frames; defaults to kotlinx.serialization.cbor.Cbor.

deltaTargets

selects the delta-target set (peers GC'd against) from full membership; defaults to the identity (full membership). A sparse-mesh GossipSeam supplies the ~k active neighbours — the partial-mesh GC scaling unlock (#654).

random

RNG for anti-entropy peer selection; defaults to kotlin.random.Random.Default. Inject a seeded instance in tests for a reproducible reconcile-peer sequence.

Samples

runTest(
    StandardTestDispatcher(),
    timeout = 5.seconds,
) {
    val loom = InMemoryLoom()
    val seamAlice = loom.host(Pattern("vote-tally"))
    val seamBob = loom.join(InMemoryTag("bob"))

    // No manual QuiltMessage.serializer(...) wrapping needed.
    val cfg = QuilterConfig(expectVirtualTime = true)
    val aliceTally = Quilter(seamAlice, PNCounter.ZERO, PNCounter.serializer(), backgroundScope, config = cfg)
    val bobTally = Quilter(seamBob, PNCounter.ZERO, PNCounter.serializer(), backgroundScope, config = cfg)

    kotlinx.coroutines.delay(1)

    // mutate removes the state.value repetition at every call site.
    aliceTally.mutate { it.increment(aliceTally.replica, 3L) }
    bobTally.mutate { it.decrement(bobTally.replica, 1L) }

    kotlinx.coroutines.delay(10)

    assertEquals(2L, aliceTally.state.value.value)
    assertEquals(aliceTally.state.value.value, bobTally.state.value.value)
}
runTest(
    StandardTestDispatcher(),
    timeout = 5.seconds,
) {
    val loom = InMemoryLoom()
    val seamA = loom.host(Pattern("sparse-mesh"))
    val seamB = loom.join(InMemoryTag("b"))

    // In a real GossipSeam this would be the active-neighbour view (~k peers).
    // Here we restrict A's delta targets to B only — C (a hypothetical third peer)
    // would converge via anti-entropy rather than blocking GC.
    val activeNeighbours: Set<PeerId> = setOf(seamB.selfId)

    val cfg = QuilterConfig(expectVirtualTime = true)
    val repA = Quilter(
        seam = seamA,
        initial = GCounter.ZERO,
        valueSerializer = GCounter.serializer(),
        scope = backgroundScope,
        config = cfg,
        deltaTargets = { _ -> activeNeighbours },  // GC against neighbours only
        random = kotlin.random.Random(seed = 42),   // reproducible anti-entropy order
    )
    val repB = Quilter(
        seam = seamB,
        initial = GCounter.ZERO,
        valueSerializer = GCounter.serializer(),
        scope = backgroundScope,
        config = cfg,
    )

    kotlinx.coroutines.delay(1)
    repA.mutate { it.inc(repA.replica, 5L) }
    kotlinx.coroutines.delay(10)

    // B converges via the normal delta path (it is in activeNeighbours).
    assertEquals(5L, repA.state.value.value)
    assertEquals(repA.state.value.value, repB.state.value.value)
}