OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store commits / 29d1bb3

firehose: add the #sync frame type (Sync 1.1 re-baseline)
syncFrame({seq, did, signedCommit, time}) → a #sync subscribeRepos frame carrying a CAR with ONLY the commit block (root = commit CID, ≤10KB), per com.atproto.sync.subscribeRepos#sync. A relay receiving #sync re-baselines the repo to that commit (re-anchors via getRepo) instead of applying incremental deltas — the sanctioned signal for a repo whose commit chain is discontinuous vs the relay's last-known state (e.g. a custodial→self-custody migration that re-authored the repo, so prev/prevData don't bridge). Added #sync to HEADERS + restampFrame so the relay re-stamps its seq like any frame. Test: 25/25. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
29d1bb3d032e5a60bac701c579b7188b6d0c1ea8
by plankatron <fleeky@gmail.com> · 2026-07-04T16:11:39.000Z

Changed files (2) +38 -3

lib/firehose.js +15 -2
……@@ -7,12 +7,14 @@
77
88import * as dagCbor from '@ipld/dag-cbor'
99import { writeCar } from 'atproto-repo/lib/repo.js'
10import { commitCid } from 'atproto-repo/lib/commit.js'
1011
1112// subscribeRepos frame = dag-cbor(header {t, op:1}) ++ dag-cbor(body). Each header is
1213// a constant byte prefix (dag-cbor canonicalises map order), so we emit/detect by it.
1314export const COMMIT_HEADER = dagCbor.encode({ t: '#commit', op: 1 })
1415export const IDENTITY_HEADER = dagCbor.encode({ t: '#identity', op: 1 })
1516export const ACCOUNT_HEADER = dagCbor.encode({ t: '#account', op: 1 })
17export const SYNC_HEADER = dagCbor.encode({ t: '#sync', op: 1 })
1618
1719function frame (header, body) {
1820 const bodyBytes = dagCbor.encode(body)
……@@ -56,7 +58,18 @@ export function accountFrame ({ seq, did, active = true, status, time }) {
5658 return frame(ACCOUNT_HEADER, body)
5759}
5860
59const HEADERS = [['#commit', COMMIT_HEADER], ['#identity', IDENTITY_HEADER], ['#account', ACCOUNT_HEADER]]
61// #sync (Sync 1.1) — tells a relay to RE-BASELINE the repo to `signedCommit` (re-anchor via
62// getRepo) instead of applying incremental deltas. This is the sanctioned signal for a repo
63// whose commit chain is discontinuous vs the relay's last-known state (e.g. a custodial→
64// self-custody migration that re-authored the repo). `blocks` is a CAR carrying ONLY the
65// commit block (root = its CID), ≤10KB. `rev`/`did` MUST match the commit object.
66export async function syncFrame ({ seq, did, signedCommit, time }) {
67 const cid = commitCid(signedCommit)
68 const blocks = await writeCar(cid, [{ cid, bytes: dagCbor.encode(signedCommit) }])
69 return frame(SYNC_HEADER, { seq, did, blocks, rev: signedCommit.rev, time })
70}
71
72const HEADERS = [['#commit', COMMIT_HEADER], ['#identity', IDENTITY_HEADER], ['#account', ACCOUNT_HEADER], ['#sync', SYNC_HEADER]]
6073
6174// Re-stamp a frame's `seq` (relay egress): a relay aggregates many PDS firehoses and MUST
6275// present its own monotonic seq in each frame (the cursor contract). Decodes, replaces
……@@ -64,7 +77,7 @@ const HEADERS = [['#commit', COMMIT_HEADER], ['#identity', IDENTITY_HEADER], ['#
6477export function restampFrame (frameBytes, seq) {
6578 const d = decodeFrame(frameBytes)
6679 if (!d) return frameBytes
67 const header = d.t === '#commit' ? COMMIT_HEADER : d.t === '#identity' ? IDENTITY_HEADER : ACCOUNT_HEADER
80 const header = d.t === '#commit' ? COMMIT_HEADER : d.t === '#identity' ? IDENTITY_HEADER : d.t === '#account' ? ACCOUNT_HEADER : SYNC_HEADER
6881 d.body.seq = seq
6982 const bodyBytes = dagCbor.encode(d.body)
7083 const out = new Uint8Array(header.length + bodyBytes.length)
test/firehose.test.js +23 -1
……@@ -8,9 +8,10 @@ import { join } from 'node:path'
88import { tmpdir } from 'node:os'
99import { RepoStore } from '../lib/repo-store.js'
1010import { RepoManager } from '../lib/repo-manager.js'
11import { commitFrame, decodeCommitFrame, COMMIT_HEADER } from '../lib/firehose.js'
11import { commitFrame, decodeCommitFrame, decodeFrame, syncFrame, restampFrame, COMMIT_HEADER, SYNC_HEADER } from '../lib/firehose.js'
1212import { isValidTid } from '../lib/tid.js'
1313import { readCar, loadCommit } from 'atproto-repo/lib/repo.js'
14import { commitCid } from 'atproto-repo/lib/commit.js'
1415import { getRecord } from '../lib/read-view.js'
1516
1617const DID = 'did:plc:yu4kgcktm7qtrwo5ofyiaf4c'
……@@ -79,6 +80,27 @@ test('delete: op cid null, rev advances, read-view reflects removal', async (t)
7980 'delete: op cid null, rev advances, read-view reflects removal')
8081})
8182
83// 4b. #sync frame (Sync 1.1 re-baseline): header byte-exact, body fields, CAR carries the
84// commit block (root = commit CID), rev matches, and restampFrame updates its seq.
85test('#sync frame: header + body + CAR-with-commit + restamp', async (t) => {
86 const { mgr } = await newMgr(t)
87 const w = await mgr.applyWrite({ action: 'create', collection: 'app.bsky.feed.post', rkey: '3sss', record: { $type: 'app.bsky.feed.post', text: 's', createdAt: iso } })
88 const signed = w.signedCommit
89 const frame = await syncFrame({ seq: 7, did: DID, signedCommit: signed, time: iso })
90 const headerOk = Buffer.from(frame.subarray(0, SYNC_HEADER.length)).equals(Buffer.from(SYNC_HEADER))
91 const dec = decodeFrame(frame)
92 const car = await readCar(dec.body.blocks)
93 const commit = loadCommit(car)
94 const cid = commitCid(signed)
95 const restamped = decodeFrame(restampFrame(frame, 99))
96 t.ok(
97 headerOk && dec.t === '#sync' &&
98 dec.body.did === DID && dec.body.seq === 7 && dec.body.rev === signed.rev &&
99 car.roots[0].equals(cid) && commit.value.rev === signed.rev && commit.value.did === DID &&
100 restamped.body.seq === 99,
101 '#sync frame: header + body + CAR-with-commit + restamp')
102})
103
82104// The live subscribeRepos stream leaves a refed TLSSocket that ws.close() doesn't
83105// release; brittle ends the run on `beforeExit` (loop drain), so unref it or the
84106// runner hangs after all asserts pass. (This is why the pre-brittle version force-exited.)