mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-07-19 11:09:26 +00:00
refactor(core): make WakuMessage a ref object with structural ==/clone
Change WakuMessage from a value object to a ref object so assignments, async-closure captures, and Result/Option bind-outs cost a pointer + refcount instead of deep-copying the three seqs + string. - structural `==` (nil-aware, field-wise) so message equality assertions keep working under ref identity - `clone()` explicit deep copy for the rare mutate-a-held-message case - rewrite `ensureTimestampSet` to be non-mutating (clone when unset) instead of `result = message; result.timestamp = ...`, which under ref semantics would mutate the shared input - new unit tests: structural == incl. nil, clone independence, ensureTimestampSet same-ref/fresh-ref behaviour Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a4da679f2
commit
1339be1f5e
@ -9,7 +9,20 @@ import ../topics, ../time
|
||||
|
||||
const MaxMetaAttrLength* = 64 # 64 bytes
|
||||
|
||||
type WakuMessage* = object # Data payload transmitted.
|
||||
type WakuMessage* = ref object
|
||||
# Data payload transmitted.
|
||||
## `WakuMessage` is a `ref object` for performance: every assignment,
|
||||
## async-closure capture, and Result/Option bind-out costs a pointer +
|
||||
## refcount instead of deep-copying the three seqs + the string.
|
||||
##
|
||||
## By convention a `WakuMessage` is **immutable after construction**. Only
|
||||
## mutate an instance you just constructed yourself, or one you obtained via
|
||||
## `clone()`. Mutating a message received through an API or held elsewhere is
|
||||
## a shared-state bug under ref semantics.
|
||||
##
|
||||
## NOTE: this deliberately deviates from the AGENTS.md `Ref`-suffix naming
|
||||
## rule; renaming the ~2,100 occurrences of `WakuMessage` has no semantic
|
||||
## value and would be pure churn.
|
||||
payload*: seq[byte]
|
||||
# String identifier that can be used for content-based filtering.
|
||||
contentTopic*: ContentTopic
|
||||
@ -28,7 +41,36 @@ type WakuMessage* = object # Data payload transmitted.
|
||||
# attribute will be used in the rln-relay protocol.
|
||||
proof*: seq[byte]
|
||||
|
||||
proc `==`*(a, b: WakuMessage): bool =
|
||||
## Structural equality. Two messages are equal when both are nil, or both are
|
||||
## non-nil with field-wise-equal contents (ref identity is *not* used).
|
||||
if a.isNil() or b.isNil():
|
||||
return a.isNil() and b.isNil()
|
||||
a.payload == b.payload and a.contentTopic == b.contentTopic and a.meta == b.meta and
|
||||
a.version == b.version and a.timestamp == b.timestamp and a.ephemeral == b.ephemeral and
|
||||
a.proof == b.proof
|
||||
|
||||
proc clone*(msg: WakuMessage): WakuMessage =
|
||||
## Explicit deep copy into a fresh ref (copies the payload/meta/proof seqs).
|
||||
## Cloning is the deliberate, rare escape hatch when a held message must be
|
||||
## mutated without affecting other holders.
|
||||
if msg.isNil():
|
||||
return nil
|
||||
WakuMessage(
|
||||
payload: msg.payload,
|
||||
contentTopic: msg.contentTopic,
|
||||
meta: msg.meta,
|
||||
version: msg.version,
|
||||
timestamp: msg.timestamp,
|
||||
ephemeral: msg.ephemeral,
|
||||
proof: msg.proof,
|
||||
)
|
||||
|
||||
proc ensureTimestampSet*(message: WakuMessage): WakuMessage =
|
||||
result = message
|
||||
if result.timestamp == 0:
|
||||
result.timestamp = getNowInNanosecondTime()
|
||||
## Returns `message` unchanged when its timestamp is already set; otherwise
|
||||
## returns a fresh clone with the current timestamp. Never mutates the input
|
||||
## (which may be shared under ref semantics).
|
||||
if message.timestamp != 0:
|
||||
return message
|
||||
result = message.clone()
|
||||
result.timestamp = getNowInNanosecondTime()
|
||||
|
||||
@ -6,6 +6,7 @@ import ./test_waku
|
||||
import
|
||||
./waku_core/test_namespaced_topics,
|
||||
./waku_core/test_time,
|
||||
./waku_core/test_message,
|
||||
./waku_core/test_message_digest,
|
||||
./waku_core/test_peers,
|
||||
./waku_core/test_published_address
|
||||
|
||||
72
tests/waku_core/test_message.nim
Normal file
72
tests/waku_core/test_message.nim
Normal file
@ -0,0 +1,72 @@
|
||||
{.used.}
|
||||
|
||||
import std/times, stew/byteutils, testutils/unittests
|
||||
import logos_delivery/waku/waku_core/message/message
|
||||
|
||||
suite "Waku Message - ref semantics":
|
||||
test "structural == on equal contents":
|
||||
let a = WakuMessage(
|
||||
payload: "\x01\x02\x03".toBytes(),
|
||||
contentTopic: "/test/1/proto",
|
||||
meta: @[byte 9],
|
||||
version: 1,
|
||||
timestamp: 42,
|
||||
ephemeral: true,
|
||||
proof: @[byte 7],
|
||||
)
|
||||
let b = WakuMessage(
|
||||
payload: "\x01\x02\x03".toBytes(),
|
||||
contentTopic: "/test/1/proto",
|
||||
meta: @[byte 9],
|
||||
version: 1,
|
||||
timestamp: 42,
|
||||
ephemeral: true,
|
||||
proof: @[byte 7],
|
||||
)
|
||||
check:
|
||||
a == b # equal by content, distinct refs
|
||||
not (a == WakuMessage(payload: "\x01\x02\x03".toBytes()))
|
||||
|
||||
test "structural == nil cases":
|
||||
let nilA: WakuMessage = nil
|
||||
let nilB: WakuMessage = nil
|
||||
let nonNil = WakuMessage(payload: @[byte 1])
|
||||
check:
|
||||
nilA == nilB # both nil -> equal
|
||||
not (nilA == nonNil) # one nil -> not equal
|
||||
not (nonNil == nilB)
|
||||
|
||||
test "clone is an independent deep copy":
|
||||
let orig =
|
||||
WakuMessage(payload: @[byte 1, 2, 3], contentTopic: "/a/1", proof: @[byte 8])
|
||||
let cp = orig.clone()
|
||||
check:
|
||||
cp == orig # equal contents
|
||||
not cp.isNil()
|
||||
# mutating the clone must not touch the original
|
||||
cp.payload = @[byte 9]
|
||||
cp.proof = @[byte 0]
|
||||
check:
|
||||
orig.payload == @[byte 1, 2, 3]
|
||||
orig.proof == @[byte 8]
|
||||
not (cp == orig)
|
||||
|
||||
test "clone(nil) is nil":
|
||||
let n: WakuMessage = nil
|
||||
check n.clone().isNil()
|
||||
|
||||
test "ensureTimestampSet returns same ref when timestamp already set":
|
||||
let msg = WakuMessage(payload: @[byte 1], timestamp: 123)
|
||||
let res = msg.ensureTimestampSet()
|
||||
check:
|
||||
cast[pointer](res) == cast[pointer](msg) # same instance, no clone
|
||||
res.timestamp == 123
|
||||
|
||||
test "ensureTimestampSet returns a fresh ref, never mutating the shared input":
|
||||
let msg = WakuMessage(payload: @[byte 1], timestamp: 0)
|
||||
let res = msg.ensureTimestampSet()
|
||||
check:
|
||||
cast[pointer](res) != cast[pointer](msg) # distinct instance
|
||||
res.timestamp != 0
|
||||
msg.timestamp == 0 # input untouched (would be a shared-state bug)
|
||||
res.payload == msg.payload
|
||||
Loading…
x
Reference in New Issue
Block a user