diff --git a/packages/api/README.md b/packages/api/README.md index 112818d..b075c03 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -81,26 +81,17 @@ Jetstream's words, delivery is at-least-once, so replaying the same create or th twice leaves the index unchanged — because the whole index can be thrown away and rebuilt from the network at any time. -### Deviation from the draft spec: Jetstream's live API has moved - -The draft spec (`api-spec.md`) assumed Jetstream's legacy `/subscribe` endpoint -(`wantedCollections=...&cursor=`). Reading the current docs -(bsky.network/docs/jetstream, 2026-11-06) instead of relying on memory, as the task asked, found -a v2 API: `wss:///xrpc/network.bsky.jetstream.subscribeEvents`, with a repeated -`collections` param (confirmed live, documented as supporting an -`app.bsky.feed.*`-style NSID-prefix wildcard, so `run.nibling.*` would also work), `kinds`, and a -`cursor` that takes either a `seq` or a `time_us`, told apart by magnitude. The index stores a -`time_us`, since that also gives the cursor's age; a live probe on 2026-11-06 with a `time_us` -two minutes old replayed from two minutes back. `src/index/jetstream.ts` targets the verified v2 shape; `wrangler.jsonc`'s -`JETSTREAM_URL` points at `wss://jetstream.us-east.bsky.network` accordingly. A live, read-only -connection to that URL with this worker's exact query string opened cleanly and returned zero -`run.nibling.*` commits in an 8-second window, which is expected: nibling.run has no real users -yet. - -`com.atproto.sync.listReposByCollection` itself matched the spec and current docs exactly -(`collection`, `limit`, `cursor` in; `{ repos: [{did}], cursor }` out); a live, read-only request -against `relay1.us-east.bsky.network` for `run.nibling.project` returned `{"repos":[]}`, as -expected for a collection nobody writes to yet. +### Jetstream v2 + +Jetstream's live API is `wss:///xrpc/network.bsky.jetstream.subscribeEvents`, with a +repeated `collections` param (an `app.bsky.feed.*`-style NSID-prefix wildcard works, so +`run.nibling.*` does too), `kinds`, and a `cursor` that takes either a `seq` or a `time_us`, told +apart by magnitude. The index stores a `time_us`, since that also gives the cursor's age. +`src/index/jetstream.ts` targets this v2 shape; `wrangler.jsonc`'s `JETSTREAM_URL` points at +`wss://jetstream.us-east.bsky.network` accordingly. + +`com.atproto.sync.listReposByCollection` takes `collection`, `limit`, `cursor` and returns +`{ repos: [{did}], cursor }`. ## Local dev @@ -121,8 +112,4 @@ The Jetstream parser and ingest tests run on frames captured from the live v2 en writes `run.nibling.*` yet, the `run.nibling.*` fixtures keep a captured envelope and change only `collection` and `record`. -A smoke run of `wrangler dev --local` confirmed `GET /oauth/client-metadata.json` (200, the exact -D19 scope string) and `GET /xrpc/run.nibling.actor.getKnown?dids=did:plc:test` (200, -`{"dids":[]}`, with CORS and cache headers) both serve correctly end to end. - This worker is never deployed from a development session: no `wrangler deploy`, ever. diff --git a/packages/api/src/index/jetstream.ts b/packages/api/src/index/jetstream.ts index da33641..e9f51ce 100644 --- a/packages/api/src/index/jetstream.ts +++ b/packages/api/src/index/jetstream.ts @@ -10,15 +10,11 @@ function isIndexCollection(value: string): value is IndexCollection { } /** - * Builds the live-tail subscribe URL. - * - * Deviation from the api-spec draft: the spec assumed Jetstream's legacy `/subscribe` endpoint - * (`wantedCollections=...&cursor=`). The current docs (bsky.network/docs/jetstream, - * read 2026-11-06) describe a v2 API at `/xrpc/network.bsky.jetstream.subscribeEvents` with - * repeated `collections` params, `kinds`, and a `cursor` that's either a `seq` or a `time_us`, - * told apart by magnitude. The index passes a `time_us` (see ingest.ts). `collections` does document NSID-prefix wildcards - * ("an `app.bsky.feed.*`-style namespace wildcard"), so `run.nibling.*` would also work, but - * since there are only four collections to ask for, this lists them explicitly instead. + * Jetstream v2 is `/xrpc/network.bsky.jetstream.subscribeEvents` with repeated `collections` + * params, `kinds`, and a `cursor` that's either a `seq` or a `time_us`, told apart by magnitude. + * The index passes a `time_us` (see ingest.ts). `collections` supports NSID-prefix wildcards + * (an `app.bsky.feed.*`-style namespace wildcard), so `run.nibling.*` would also work, but since + * there are only four collections to ask for, this lists them explicitly instead. */ export function subscribeUrl(baseUrl: string, cursor?: string): string { const url = new URL('/xrpc/network.bsky.jetstream.subscribeEvents', baseUrl); diff --git a/packages/api/tests/backfill.test.ts b/packages/api/tests/backfill.test.ts index ae0e0b1..82de17b 100644 --- a/packages/api/tests/backfill.test.ts +++ b/packages/api/tests/backfill.test.ts @@ -40,7 +40,6 @@ test('backfillCollection pages listReposByCollection and listRecords, and valida value: { $type: 'run.nibling.project', name: 'Alice Project', files: [], createdAt: '2026-11-01T00:00:00.000Z' }, }, { - // fails validation (no `name`): dropped, not thrown uri: 'at://did:plc:alice/run.nibling.project/3la2', cid: 'bafy2', value: { $type: 'run.nibling.project', files: [], createdAt: '2026-11-01T00:00:00.000Z' }, @@ -48,7 +47,6 @@ test('backfillCollection pages listReposByCollection and listRecords, and valida ], }); } - // bob's PDS is unreachable throw new Error('network down'); } diff --git a/packages/api/tests/model.test.ts b/packages/api/tests/model.test.ts index be1c2f3..059b074 100644 --- a/packages/api/tests/model.test.ts +++ b/packages/api/tests/model.test.ts @@ -127,8 +127,8 @@ test('delete removes the project from recent', () => { test('a like on a not-yet-indexed project still counts once the project appears', () => { const model = new IndexModel(sqliteSql()); - model.apply(fixtureEvents()[1]!); // like1, subject = project1, before project1 is indexed - model.apply(fixtureEvents()[0]!); // project1 now appears + model.apply(fixtureEvents()[1]!); + model.apply(fixtureEvents()[0]!); const { projects } = model.recent(); assert.equal(projects[0]!.likeCount, 1); @@ -149,13 +149,7 @@ test('known returns exactly the DIDs with a project or profile record', () => { const model = new IndexModel(sqliteSql()); for (const event of fixtureEvents()) model.apply(event); - const known = model.known([ - 'did:plc:alice', // has a project - 'did:plc:bob', // only has a like - 'did:plc:dave', // has a project - 'did:plc:erin', // only has a profile - 'did:plc:frank', // has nothing - ]); + const known = model.known(['did:plc:alice', 'did:plc:bob', 'did:plc:dave', 'did:plc:erin', 'did:plc:frank']); assert.deepStrictEqual(known, ['did:plc:alice', 'did:plc:dave', 'did:plc:erin']); });