Add support for Jetstream V2 #83
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET#83
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Pushed. Here's the reply:
Added support for the Jetstream v2 wire protocol on branch
claude/issue-83.What v2 is
Jetstream's second wire serves at
/xrpc/network.bsky.jetstream.subscribeEventsunder thexrpc.v1.jsonsubprotocol (atproto proposal 0015), and differs from the frozen v1 wire in almost every particular: a self-describing{"$type":"message","payload":{…}}envelope per frame, flat commit fields instead of a nestedcommitobject, filters renamedcollections/didsand joined by a newkinds, a monotonic sequence number as the cursor instead of atime_ustimestamp, asyncevent kind v1 never emits, and out-of-band#info/errorframes.JetstreamClientandJetstreamConsumernow speak both, selected by the newJetstreamConsumerOptions.Protocol.What changed
Protocoldefaults toJetstreamProtocol.V1— every existing configuration keeps its endpoint, parameter names, and timestamp cursor with no change. The legacyjetstream1/jetstream2hosts serve v1 only; the two v2 hosts serve both.JetstreamEndpoints—UsEast/UsWest(v2) and the four legacy hosts, instead of pasting host names.WantedKinds— the v2kindsfilter. A collection filter constrains commit events only (identity/account/sync flow regardless, on both protocols), so combining it with aWantedKindslist that excludesCommitis a filter that could never apply — that now throws before the socket opens rather than coming back as an HTTP 400.JetstreamSyncEvent— resync marker withRevand the commit CAR inBlocks, decoded from the wire's$byteswrapper.JetstreamEvent.Cursor— the sequence number. Also populated on the v1 wire from thecursorfield the v2 hosts added to it (null against a legacy instance). PlusTimestampas aDateTimeOffset; on v2 both derive from the RFC 3339time, whose six fractional digits convert exactly.LastCursoralongsideLastTimeUs, persists whichever the protocol uses, and reconnects at exactly the last sequence number, since a v2 cursor is replayed inclusively andReconnectRewindhas nothing to rewind past.CursorTooOld), retired dictionary, or malformed filter with an HTTP 400. That surfaces as the newJetstreamConnectException(StatusCode,IsRetryable); the consumer persists progress and rethrows, because retrying is futile and dropping the cursor would silently skip the gap you're meant to backfill.OnInfo/OnStreamError— v2's#infoadvisories and terminalerrorframes are neither events nor exceptions; they're logged and passed to these.JetstreamDictionaryClient— v2 compression is dictionary-versioned, so it fetchesgetZstdDictionaryand reads the ID out of the zstd dictionary's own header, yielding both halves of the newZstdDictionaryId+Decompressorpair in one call.JetstreamEventParser.ParseFrame(json, protocol)is the new entry point returning aJetstreamFrame; the existingParse(...)overloads are unchanged.Deliberately out of scope
v2 hosts also serve an authenticated archive/replay API (
planSnapshot/listSegments/getSegment/getBlockover a columnar.jsssegment format, with cutover to the live tail). That's a much larger surface than the live tail — it needs a.jssdecoder and API-key metering — so I implemented the live tail only.docs/jetstream.mdsays so explicitly and points at Bluesky's own SDKs for the archive portion.Verification
dotnet buildacross the solution: 0 warnings, 0 errors.dotnet test tests/ATProtoNet.Tests/: 1237 passed, 0 failed, 5 gated skips (same as baseline) — up from 1228, with 4 new test files covering v2 parsing, URL building, consumer cursor semantics, and the dictionary client.jetstream.us-east/jetstream.us-west): commit/identity/account parsing with correct seq, microsecond time, URI, rev and CID; thekindsfilter (zero commits over 25 s where the unfiltered stream runs hundreds/sec); inclusive sequence-number resume (asked24707154431, got exactly that); the pre-upgrade 400 forcursor=1mapping to a non-retryableJetstreamConnectException; the dictionary fetch (id20260811, 64 KiB); and confirmation that the v1 wire on a v2 host does carry thecursorfield the SDK now surfaces. Nosyncevent happened to occur during the window — that path is covered by unit tests only.CHANGELOG.mdupdated under[Unreleased] → Added; docs updated indocs/jetstream.md(new Protocol Versions, Advisory and error frames, and Historical Replay sections),docs/api-reference.md,docs/index.md, andREADME.md.⏱ 17m 9s · 66 turns · tokens: 179.7k in / 81.3k out (+7240.6k cache-read)
📜 Full session transcript
→ Opened PR: #84
@claude-bot Okay, but please open another issue for that archive/replay API, I wanna support it eventually.
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Opened issue #85 — Add support for the Jetstream v2 archive/replay API (labelled
enhancement), tracking the piece #84 left out.Rather than just restating "the archive exists", I read Bluesky's Network Replay docs and the
.jssdata-layout spec and wrote the issue up as something implementable:planSnapshotpinningsealedTipSeq, re-plan whileplannedThroughSeq < S, download per-segmentmode: "segment"vsmode: "blocks", then connect the live socket once at?cursor=Swith seq dedup because the cursor is inclusive..jssformat — 256-byte header, length-prefixed zstd blocks, footer with block index and DID blooms; the columnar block layout down to the individual columns and thekinddiscriminator. This is the bulk of the work and the reason it didn't ride along.429+Retry-After, and byte-offsetRangeresume, which needs to be designed in rather than bolted on.at://URI and account-level markers must not be dropped.JetstreamArchiveClient,JetstreamSegmentReader,JetstreamReplayConsumer, cursor reuse viaIFirehoseCursorStore), a five-step phasing that puts the offline-testable.jssreader first, and four open design questions.It also notes the failure mode where a long backfill lets
Sage out of the 36-hour lookback window — that already surfaces as theJetstreamConnectExceptionfrom #84, so the handling is "re-enter the plan loop", not "retry".No code changes and no branch this time; #84 is unchanged. Say the word if you'd like me to add a cross-reference to #85 from the Historical Replay section of
docs/jetstream.mdonclaude/issue-83before that PR merges.⏱ 2m 54s · 23 turns · tokens: 41.5k in / 9.9k out (+732.7k cache-read)
📜 Full session transcript