7.7 KiB
Capability Spec Delta: crdt (crdt-completion)
Package: @suaveplan/crdt
Status: PROPOSED
Every requirement below carries a
**Verified by:**line at authoring time. No exceptions. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: G-Set grow-only set CRDT
The package SHALL expose a GSet<T> class implementing CRDT<Set<T>, GSetState<T>>. The class SHALL support add(value: T): void, has(value: T): boolean, value(): Set<T>, state(): GSetState<T>, merge(remote: GSetState<T>): void, toJSON(): string, fromJSON(json: string): void, and clone(): GSet<T>. merge SHALL be a set union — idempotent, commutative, and associative. The state envelope SHALL include type: "g-set" for cross-CRDT serialisation discrimination.
Scenario: add then has returns true
- WHEN
gset.add("a")is called and thengset.has("a")is called - THEN the result is
true
Scenario: merge is set union
- WHEN
gset1holds{"a", "b"}andgset2holds{"b", "c"}andgset1.merge(gset2.state())is called - THEN
gset1.value()equalsSet { "a", "b", "c" }
Scenario: merge is idempotent
- WHEN
gset1.merge(gset2.state())is called twice in succession - THEN the second call leaves
gset1.value()unchanged from after the first call
Scenario: merge is commutative
- WHEN two pairs of merges are performed in opposite orders against fresh clones
- THEN the resulting
value()sets are deeply equal
Scenario: merge is associative
- WHEN
(a ∪ b) ∪ cis computed against fresh clones anda ∪ (b ∪ c)is computed against another set of fresh clones - THEN the resulting
value()sets are deeply equal
Scenario: toJSON / fromJSON round-trip
- WHEN a populated
GSetis serialised viatoJSON()and a freshGSetcallsfromJSON(json) - THEN the fresh instance's
value()equals the original'svalue()
Scenario: initialElements seeds the set
- WHEN
new GSet<string>("node-1", { initialElements: ["a", "b", "c"] })is constructed - THEN
gset.value()equalsSet { "a", "b", "c" }immediately
Scenario: BigInt element rejected
- WHEN
gset.add(BigInt(1) as never)is called - THEN
CrdtErroris thrown with codeGSET_INVALID_ELEMENTandcontext.valueset to the offending input
Verified by: packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"adds an element and reports has() true" AND packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"merge is set union" AND packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"merge is idempotent" AND packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"merge is commutative" AND packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"merge is associative" AND packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"toJSON / fromJSON round-trip" AND packages/flow/crdt/src/crdts/g-set/g-set.test.ts::"initialElements seeds the set" AND packages/flow/crdt/src/crdts/g-set/g-set.spec-conformance.test.ts::"throws GSET_INVALID_ELEMENT for BigInt element"
Requirement: LWW-Map last-write-wins map CRDT
The package SHALL expose a LWWMap<V> class implementing CRDT<Map<string, V>, LWWMapState<V>>. The class SHALL support set(key: string, value: V, ts?: number): void, get(key: string): V | undefined, has(key: string): boolean, delete(key: string, ts?: number): boolean, keys(): string[], value(): Map<string, V>, state(): LWWMapState<V>, merge(remote: LWWMapState<V>): void, toJSON(): string, fromJSON(json: string): void, clone(): LWWMap<V>, and reset(): void.
The constructor SHALL accept an optional injectable Clock from @suaveplan/types; when an explicit ts argument is omitted from set or delete, the clock's now() SHALL provide it. Tie-break on equal timestamp SHALL compare nodeId lexicographically (matching LWWRegister). Deletes SHALL leave a tombstone preserving the timestamp; subsequent set with a strictly greater timestamp SHALL clear the tombstone. merge SHALL respect tombstones across both directions: remote entries older than a local tombstone are dropped; remote tombstones newer than a local entry remove the entry. merge SHALL be idempotent, commutative, and associative.
Scenario: set then get returns the value
- WHEN
map.set("k", "v")is called and thenmap.get("k")is called - THEN the result is
"v"
Scenario: older set is no-op
- WHEN
map.set("k", "v1", 100)is called and thenmap.set("k", "v0", 50)is called - THEN
map.get("k")returns"v1"
Scenario: tie-break on equal timestamp uses nodeId
- WHEN two replicas with
nodeId = "a"andnodeId = "b"both writekey="k"at the same timestamp100 - THEN the merged result is the value written by
nodeId = "b"(lexicographically greater)
Scenario: delete leaves a tombstone
- WHEN
map.delete("k", 100)is called - THEN
map.has("k")returnsfalseANDmap.get("k")returnsundefinedAND the internal state contains a tombstone with timestamp100
Scenario: re-add after delete
- WHEN
map.delete("k", 100)is called and thenmap.set("k", "v", 200)is called - THEN
map.get("k")returns"v"AND the tombstone is cleared
Scenario: merge respects tombstones — older remote entry dropped
- WHEN local has tombstone for
"k"at ts200and remote state has entry{k: { value: "v", timestamp: 100 }} - THEN after
local.merge(remote.state()),local.has("k")returnsfalse
Scenario: merge respects tombstones — newer remote tombstone removes local entry
- WHEN local has entry
{k: "v", ts: 100}and remote has tombstone for"k"at ts200 - THEN after
local.merge(remote.state()),local.has("k")returnsfalse
Scenario: injected Clock provides timestamps
- WHEN
new LWWMap("node-1", { clock: { now: () => 555 } })is constructed andmap.set("k", "v")is called without a ts argument - THEN the resulting entry has
timestamp: 555
Scenario: merge is idempotent / commutative / associative
- WHEN the property test runs over 100 random key/value/timestamp triples per replica
- THEN
mergesatisfies all three CRDT properties
Scenario: reset clears state
- WHEN
map.reset()is called against a populated map - THEN
map.value().size === 0AND no tombstones remain
Scenario: non-string key rejected
- WHEN
map.set(BigInt(1) as never, "v")is called - THEN
CrdtErroris thrown with codeLWWMAP_INVALID_KEY
Verified by: packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"set then get returns the value" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"set with explicit timestamp respects ordering" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"tie-break on equal timestamp uses nodeId" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"delete leaves a tombstone" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"set after delete with greater ts clears tombstone" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"merge respects tombstones — older remote entry dropped" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"merge respects tombstones — newer remote tombstone removes local entry" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"merge is idempotent / commutative / associative" AND packages/flow/crdt/src/crdts/lww-map/lww-map.test.ts::"reset clears entries and tombstones" AND packages/flow/crdt/src/crdts/lww-map/lww-map.spec-conformance.test.ts::"set without ts pulls from injected clock" AND packages/flow/crdt/src/crdts/lww-map/lww-map.spec-conformance.test.ts::"throws LWWMAP_INVALID_KEY for non-string key"