jetstream client
atproto jetstream client
jetstream examples streamplace_chat.md
4.4 kB

replay a Streamplace chat #

Streamplace chat messages are ordinary ATProto records in each chatter's repo. Every place.stream.chat.message record names the streamer's DID in its streamer field, so a Jetstream consumer can reconstruct a broadcast's chat.

choose a broadcast #

Pass a handle to replay the account's latest broadcast and then follow it live:

zig build example-streamplace-chat -- iame.li

Pass the exact livestream AT-URI to select a historical broadcast:

zig build example-streamplace-chat -- \
  'at://did:plc:2zmxikig2sj7gqaezl5gntae/place.stream.livestream/3mst2znymsb2e'

The second form starts at that exact place.stream.livestream record on the streamer's PDS. Streamplace can roll a single viewing session into another livestream record, so the example also walks newer records and joins an immediately contiguous segment for the same media URL (a rolled-over session prints each joined record). The first record's TID supplies the lower replay boundary and the final segment's endedAt supplies the upper chat boundary, with five minutes of room for delayed ingestion.

logical Streamplace session:
- Loop engineering with @brittanyellich.com!
  2026-08-11T16:48:57Z -> 2026-08-11T16:58:53.645Z
  at://did:plc:.../place.stream.livestream/3mst2znymsb2e

replaying from the archive (https://stream.waow.tech)...

[2026-08-11T16:49:21.783Z] did:plc:...: ooo yeha i setup ssh between macs ...
[2026-08-11T16:49:39.355Z] did:plc:...: i know how to setup some secure ...
...
archive replay: 17 messages from 633 blocks
finishing the session tail over the live socket...

replay complete: 17 observed messages across 1 segment

The output uses author DIDs because Jetstream carries repo writes, not hydrated profiles. The implementation is in examples/streamplace_chat.zig and is compile-checked by zig build. Build it with -Doptimize=ReleaseSafe — the archive pass decompresses hundreds of zstd blocks, which is slow in Debug.

Historical chat comes from zat.ArchiveBackfill against stream.waow.tech's full-network archive, so replay does not depend on Jetstream cursor retention. The session's time window maps to plan seq bounds (fetchSeqBounds), so a recent broadcast plans only the handful of blocks it touches. Sessions inside the archive's merged bootstrap region (before 2026-08-04) still replay, but their rows are scattered per-DID, so the plan spans more blocks — expect a bulk download, and watch the archive backfill: N blocks decoded progress lines. The newest events sit in the archive's unsealed tail, so the example always finishes over the live socket from the covered boundary; the two passes overlap at the seam and the printer dedups by rkey.

Replay reflects the archive's contents: records whose authors later deleted them are folded out by the archive's delete-compaction (the .delete events remain), and an instance's own documented coverage bounds apply.

For current-state discovery without an archive, use the same shape as the collection_creators flow: enumerate every repo containing place.stream.chat.message, resolve each repo's PDS, fetch its current records, and filter them by streamer and time. That cannot recover deleted records. A durable chat archive should still consume continuously and persist both matching records and its cursor.

Streamplace's /api/websocket/<streamer-did> endpoint is useful when hydrated author profiles, moderation events, and viewer counts matter more than complete history. Its initial chat burst contains only the most recent 100 messages.

The record shape is documented in Streamplace's place.stream.chat.message lexicon. The com.atproto.repo.getRecord query retrieves the selected livestream from its PDS, and the replay cursor is part of the Jetstream subscription API.

wiring zat into your own build.zig
const jetstream = b.dependency("jetstream", .{}).module("jetstream");
exe.root_module.addImport("jetstream", jetstream);

after zig fetch --save https://tangled.org/zat.dev/jetstream/archive/main.