Quilter
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
the us.tractat.kuilt.core.Seam to replicate over.
the starting CRDT state (typically the zero/empty value).
the KSerializer for S. The message serializer is derived automatically.
the CoroutineScope whose Job parents the replicator's owned child job.
this peer's ReplicaId; defaults to ReplicaId(seam.selfId.value).
replication behaviour tuning.
monotonic time source; override in tests.
binary codec for wire frames; defaults to kotlinx.serialization.cbor.Cbor.
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).
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)
}