See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add parsePostEmbed to jetstream parser; skip quote posts in consumer

Parse images/video/external embeds from raw Jetstream records into
PostEmbed types with CDN URL construction. Quote posts (record and
recordWithMedia) no longer buffer snapshots in Part A of handlePostEvent,
while Part B quote engagement detection continues to work for tracked
embedded authors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+677 -28
+1 -1
app/lib/atproto/index.ts
··· 1 1 // Pure parsers 2 2 export { parseAtUri } from './parsers/at_uri.js' 3 - export { parseJetstreamEvent } from './parsers/jetstream.js' 3 + export { parseJetstreamEvent, parsePostEmbed } from './parsers/jetstream.js' 4 4 export { parseGetAuthorFeedResponse } from './parsers/get_author_feed.js' 5 5 6 6 // Types
+182
app/lib/atproto/parsers/jetstream.ts
··· 7 7 PostDeleteEvent, 8 8 AccountEvent, 9 9 IdentityEvent, 10 + PostEmbed, 10 11 } from '../types.js' 11 12 12 13 // --------------------------------------------------------------------------- ··· 43 44 } 44 45 45 46 // --------------------------------------------------------------------------- 47 + // CDN URL helpers 48 + // --------------------------------------------------------------------------- 49 + 50 + /** Extract the CID string from a raw blob object (blob.ref.$link). */ 51 + function blobCid(blob: Record<string, unknown>): string | undefined { 52 + const ref = getObject(blob, 'ref') 53 + if (!ref) return undefined 54 + return getString(ref, '$link') 55 + } 56 + 57 + /** Construct a Bluesky CDN image URL for a given DID and CID. */ 58 + function imageUrl(did: string, cid: string, size: 'feed_thumbnail' | 'feed_fullsize'): string { 59 + return `https://cdn.bsky.app/img/${size}/plain/${did}/${cid}` 60 + } 61 + 62 + /** Construct a Bluesky video thumbnail URL for a given DID and CID. */ 63 + function videoThumbnailUrl(did: string, cid: string): string { 64 + return `https://video.bsky.app/watch/${encodeURIComponent(did)}/${cid}/thumbnail.jpg` 65 + } 66 + 67 + // --------------------------------------------------------------------------- 68 + // Post embed parsing (images / video / external from raw records) 69 + // --------------------------------------------------------------------------- 70 + 71 + /** 72 + * Parse a post record's embed field into a PostEmbed. 73 + * 74 + * Called from `parsePostCreate` for every post event. For quote-post embed 75 + * types (record / recordWithMedia), returns null — the consumer uses 76 + * `embeddedPostUri` to detect quotes instead. 77 + * 78 + * @param record - The raw post record object from the Jetstream commit 79 + * @param authorDid - The DID of the post's author (needed for CDN URL construction) 80 + * @returns A PostEmbed or null 81 + */ 82 + export function parsePostEmbed(record: unknown, authorDid: string): PostEmbed | null { 83 + if (!isObject(record)) return null 84 + 85 + const embed = getObject(record, 'embed') 86 + if (!embed) return null 87 + 88 + const embedType = getString(embed, '$type') 89 + if (!embedType) return null 90 + 91 + // Quote-post types — not rendered, handled by embeddedPostUri 92 + if (embedType === 'app.bsky.embed.record' || embedType === 'app.bsky.embed.recordWithMedia') { 93 + return null 94 + } 95 + 96 + // Images embed 97 + if (embedType === 'app.bsky.embed.images') { 98 + const rawImages = embed['images'] 99 + if (!Array.isArray(rawImages) || rawImages.length === 0) { 100 + console.warn('[parsePostEmbed] images embed has no images array') 101 + return null 102 + } 103 + 104 + const items: Array<{ 105 + thumb: string 106 + fullsize: string 107 + alt: string 108 + aspectRatio?: { width: number; height: number } 109 + }> = [] 110 + 111 + for (const img of rawImages) { 112 + if (!isObject(img)) continue 113 + const blob = getObject(img, 'image') 114 + if (!blob) { 115 + console.warn('[parsePostEmbed] image missing image blob') 116 + continue 117 + } 118 + const cid = blobCid(blob) 119 + if (!cid) { 120 + console.warn('[parsePostEmbed] image blob missing ref.$link') 121 + continue 122 + } 123 + 124 + const alt = getString(img, 'alt') ?? '' 125 + const item: { 126 + thumb: string 127 + fullsize: string 128 + alt: string 129 + aspectRatio?: { width: number; height: number } 130 + } = { 131 + thumb: imageUrl(authorDid, cid, 'feed_thumbnail'), 132 + fullsize: imageUrl(authorDid, cid, 'feed_fullsize'), 133 + alt, 134 + } 135 + 136 + const ar = getObject(img, 'aspectRatio') 137 + if (ar) { 138 + const width = getNumber(ar, 'width') 139 + const height = getNumber(ar, 'height') 140 + if (width !== undefined && height !== undefined) { 141 + item.aspectRatio = { width, height } 142 + } 143 + } 144 + 145 + items.push(item) 146 + } 147 + 148 + if (items.length === 0) { 149 + console.warn('[parsePostEmbed] images embed produced no valid items') 150 + return null 151 + } 152 + 153 + return { type: 'images', items } 154 + } 155 + 156 + // Video embed 157 + if (embedType === 'app.bsky.embed.video') { 158 + const blob = getObject(embed, 'video') 159 + if (!blob) { 160 + console.warn('[parsePostEmbed] video embed missing video blob') 161 + return null 162 + } 163 + const cid = blobCid(blob) 164 + if (!cid) { 165 + console.warn('[parsePostEmbed] video blob missing ref.$link') 166 + return null 167 + } 168 + 169 + const alt = getString(embed, 'alt') ?? '' 170 + const result: { 171 + type: 'video' 172 + thumbnail: string 173 + alt: string 174 + aspectRatio?: { width: number; height: number } 175 + } = { 176 + type: 'video', 177 + thumbnail: videoThumbnailUrl(authorDid, cid), 178 + alt, 179 + } 180 + 181 + const ar = getObject(embed, 'aspectRatio') 182 + if (ar) { 183 + const width = getNumber(ar, 'width') 184 + const height = getNumber(ar, 'height') 185 + if (width !== undefined && height !== undefined) { 186 + result.aspectRatio = { width, height } 187 + } 188 + } 189 + 190 + return result 191 + } 192 + 193 + // External embed 194 + if (embedType === 'app.bsky.embed.external') { 195 + const external = getObject(embed, 'external') 196 + if (!external) { 197 + console.warn('[parsePostEmbed] external embed missing external object') 198 + return null 199 + } 200 + 201 + const uri = getString(external, 'uri') 202 + const title = getString(external, 'title') 203 + const description = getString(external, 'description') 204 + if (uri === undefined || title === undefined || description === undefined) { 205 + console.warn('[parsePostEmbed] external embed missing uri/title/description') 206 + return null 207 + } 208 + 209 + let thumb: string | null = null 210 + const thumbBlob = getObject(external, 'thumb') 211 + if (thumbBlob) { 212 + const cid = blobCid(thumbBlob) 213 + if (cid) { 214 + thumb = imageUrl(authorDid, cid, 'feed_thumbnail') 215 + } 216 + } 217 + 218 + return { type: 'external', uri, title, description, thumb } 219 + } 220 + 221 + // Unknown embed type — not an error, just not supported 222 + return null 223 + } 224 + 225 + // --------------------------------------------------------------------------- 46 226 // Embed URI extraction (spec §6 Flow 1 — quote detection) 47 227 // --------------------------------------------------------------------------- 48 228 ··· 167 347 168 348 const postUri = `at://${did}/${collection}/${rkey}` 169 349 const embeddedPostUri = extractEmbeddedPostUri(record) 350 + const embed = parsePostEmbed(record, did) 170 351 171 352 return { 172 353 kind: 'post', ··· 176 357 text, 177 358 createdAt: new Date(createdAtStr), 178 359 embeddedPostUri, 360 + embed, 179 361 ingestedAt: timeUsToDate(timeUs), 180 362 } 181 363 }
+5
app/lib/atproto/types.ts
··· 69 69 * Captured from v1 forward for future quote-count support (spec §6 Flow 1). 70 70 */ 71 71 embeddedPostUri: string | null 72 + /** 73 + * Parsed embed (images/video/external link) from the post record. 74 + * Null for posts with no embed or quote-post embed types. 75 + */ 76 + embed: PostEmbed | null 72 77 /** When the event was ingested (from top-level time_us, converted from µs) */ 73 78 ingestedAt: Date 74 79 }
+19 -16
app/services/jetstream_consumer.ts
··· 367 367 368 368 // Part A: snapshot insert for tracked authors 369 369 if (isTrackedAuthor) { 370 - this.snapshotBuffer.push({ 371 - postUri: event.postUri, 372 - postAuthorDid: event.authorDid, 373 - postText: event.text, 374 - postCreatedAt: event.createdAt, 375 - snapshotLikes: 0, 376 - snapshotReposts: 0, 377 - snapshotQuotes: 0, 378 - snapshotTakenAt: this.deps.now(), 379 - // Embed parsing is wired up in a later task; default to null for now. 380 - embed: null, 381 - }) 370 + if (event.embeddedPostUri !== null) { 371 + // Quote post — skip snapshot, don't advance cursor; Part B handles engagement 372 + } else { 373 + this.snapshotBuffer.push({ 374 + postUri: event.postUri, 375 + postAuthorDid: event.authorDid, 376 + postText: event.text, 377 + postCreatedAt: event.createdAt, 378 + snapshotLikes: 0, 379 + snapshotReposts: 0, 380 + snapshotQuotes: 0, 381 + snapshotTakenAt: this.deps.now(), 382 + embed: event.embed, 383 + }) 382 384 383 - this.advancePendingCursor(timeUs) 385 + this.advancePendingCursor(timeUs) 384 386 385 - // Flush immediately if the snapshot buffer hits 1000 rows 386 - if (this.snapshotBuffer.length >= 1000) { 387 - void this.flushBuffer() 387 + // Flush immediately if the snapshot buffer hits 1000 rows 388 + if (this.snapshotBuffer.length >= 1000) { 389 + void this.flushBuffer() 390 + } 388 391 } 389 392 } 390 393
+334 -1
tests/unit/atproto/jetstream.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 - import { parseJetstreamEvent } from '#lib/atproto/index' 2 + import { parseJetstreamEvent, parsePostEmbed } from '#lib/atproto/index' 3 3 4 4 // --------------------------------------------------------------------------- 5 5 // Real-world Jetstream event fixtures ··· 465 465 assert.isNull(parseJetstreamEvent({ did: 'did:plc:abc', time_us: 123 })) 466 466 }) 467 467 }) 468 + 469 + // --------------------------------------------------------------------------- 470 + // parsePostEmbed tests 471 + // --------------------------------------------------------------------------- 472 + 473 + const TEST_DID = 'did:plc:z72i7hdynmk6r22z27h6tvur' 474 + 475 + test.group('parsePostEmbed — images', () => { 476 + test('parses images embed with 2 images, alt text, and aspectRatio', ({ assert }) => { 477 + const record = { 478 + $type: 'app.bsky.feed.post', 479 + text: 'some text', 480 + embed: { 481 + $type: 'app.bsky.embed.images', 482 + images: [ 483 + { 484 + alt: 'first image', 485 + aspectRatio: { height: 675, width: 1200 }, 486 + image: { 487 + $type: 'blob', 488 + ref: { $link: 'bafkreifirstcid' }, 489 + mimeType: 'image/jpeg', 490 + size: 100000, 491 + }, 492 + }, 493 + { 494 + alt: 'second image', 495 + aspectRatio: { height: 1000, width: 1000 }, 496 + image: { 497 + $type: 'blob', 498 + ref: { $link: 'bafkreisecondcid' }, 499 + mimeType: 'image/png', 500 + size: 200000, 501 + }, 502 + }, 503 + ], 504 + }, 505 + } 506 + 507 + const result = parsePostEmbed(record, TEST_DID) 508 + 509 + assert.isNotNull(result) 510 + assert.equal(result!.type, 'images') 511 + if (result?.type !== 'images') return 512 + 513 + assert.equal(result.items.length, 2) 514 + assert.equal( 515 + result.items[0].thumb, 516 + `https://cdn.bsky.app/img/feed_thumbnail/plain/${TEST_DID}/bafkreifirstcid` 517 + ) 518 + assert.equal( 519 + result.items[0].fullsize, 520 + `https://cdn.bsky.app/img/feed_fullsize/plain/${TEST_DID}/bafkreifirstcid` 521 + ) 522 + assert.equal(result.items[0].alt, 'first image') 523 + assert.deepEqual(result.items[0].aspectRatio, { width: 1200, height: 675 }) 524 + 525 + assert.equal( 526 + result.items[1].thumb, 527 + `https://cdn.bsky.app/img/feed_thumbnail/plain/${TEST_DID}/bafkreisecondcid` 528 + ) 529 + assert.equal(result.items[1].alt, 'second image') 530 + }) 531 + 532 + test('parses single image without aspectRatio', ({ assert }) => { 533 + const record = { 534 + embed: { 535 + $type: 'app.bsky.embed.images', 536 + images: [ 537 + { 538 + alt: 'no ratio', 539 + image: { 540 + $type: 'blob', 541 + ref: { $link: 'bafkreinoratio' }, 542 + mimeType: 'image/jpeg', 543 + size: 50000, 544 + }, 545 + }, 546 + ], 547 + }, 548 + } 549 + 550 + const result = parsePostEmbed(record, TEST_DID) 551 + 552 + assert.isNotNull(result) 553 + if (result?.type !== 'images') return 554 + 555 + assert.equal(result.items.length, 1) 556 + assert.isUndefined(result.items[0].aspectRatio) 557 + }) 558 + }) 559 + 560 + test.group('parsePostEmbed — video', () => { 561 + test('parses video embed with correct URL-encoded DID', ({ assert }) => { 562 + const record = { 563 + embed: { 564 + $type: 'app.bsky.embed.video', 565 + alt: 'my video', 566 + aspectRatio: { height: 912, width: 440 }, 567 + video: { 568 + $type: 'blob', 569 + ref: { $link: 'bafkreibefbrio2cu7cydqjvgg46bx63zsu4i5t6eblohtjjk3j7sotgrim' }, 570 + mimeType: 'video/mp4', 571 + size: 1234567, 572 + }, 573 + }, 574 + } 575 + 576 + const result = parsePostEmbed(record, TEST_DID) 577 + 578 + assert.isNotNull(result) 579 + assert.equal(result!.type, 'video') 580 + if (result?.type !== 'video') return 581 + 582 + const encodedDid = encodeURIComponent(TEST_DID) 583 + assert.equal( 584 + result.thumbnail, 585 + `https://video.bsky.app/watch/${encodedDid}/bafkreibefbrio2cu7cydqjvgg46bx63zsu4i5t6eblohtjjk3j7sotgrim/thumbnail.jpg` 586 + ) 587 + assert.equal(result.alt, 'my video') 588 + assert.deepEqual(result.aspectRatio, { width: 440, height: 912 }) 589 + }) 590 + 591 + test('video embed with no alt defaults to empty string', ({ assert }) => { 592 + const record = { 593 + embed: { 594 + $type: 'app.bsky.embed.video', 595 + video: { 596 + $type: 'blob', 597 + ref: { $link: 'bafkreivideocid' }, 598 + mimeType: 'video/mp4', 599 + size: 100, 600 + }, 601 + }, 602 + } 603 + 604 + const result = parsePostEmbed(record, TEST_DID) 605 + 606 + assert.isNotNull(result) 607 + if (result?.type !== 'video') return 608 + 609 + assert.equal(result.alt, '') 610 + assert.isUndefined(result.aspectRatio) 611 + }) 612 + }) 613 + 614 + test.group('parsePostEmbed — external', () => { 615 + test('parses external embed with thumb', ({ assert }) => { 616 + const record = { 617 + embed: { 618 + $type: 'app.bsky.embed.external', 619 + external: { 620 + uri: 'https://example.com', 621 + title: 'Example', 622 + description: 'An example page', 623 + thumb: { 624 + $type: 'blob', 625 + ref: { $link: 'bafkreithumbcid' }, 626 + mimeType: 'image/jpeg', 627 + size: 12345, 628 + }, 629 + }, 630 + }, 631 + } 632 + 633 + const result = parsePostEmbed(record, TEST_DID) 634 + 635 + assert.isNotNull(result) 636 + assert.equal(result!.type, 'external') 637 + if (result?.type !== 'external') return 638 + 639 + assert.equal(result.uri, 'https://example.com') 640 + assert.equal(result.title, 'Example') 641 + assert.equal(result.description, 'An example page') 642 + assert.equal( 643 + result.thumb, 644 + `https://cdn.bsky.app/img/feed_thumbnail/plain/${TEST_DID}/bafkreithumbcid` 645 + ) 646 + }) 647 + 648 + test('parses external embed without thumb', ({ assert }) => { 649 + const record = { 650 + embed: { 651 + $type: 'app.bsky.embed.external', 652 + external: { 653 + uri: 'https://example.com', 654 + title: 'No Thumb', 655 + description: 'No thumbnail here', 656 + }, 657 + }, 658 + } 659 + 660 + const result = parsePostEmbed(record, TEST_DID) 661 + 662 + assert.isNotNull(result) 663 + if (result?.type !== 'external') return 664 + 665 + assert.isNull(result.thumb) 666 + }) 667 + }) 668 + 669 + test.group('parsePostEmbed — quote types return null', () => { 670 + test('record embed returns null', ({ assert }) => { 671 + const record = { 672 + embed: { 673 + $type: 'app.bsky.embed.record', 674 + record: { 675 + uri: 'at://did:plc:someone/app.bsky.feed.post/abc', 676 + cid: 'bafyrecordcid', 677 + }, 678 + }, 679 + } 680 + 681 + const result = parsePostEmbed(record, TEST_DID) 682 + assert.isNull(result) 683 + }) 684 + 685 + test('recordWithMedia embed returns null', ({ assert }) => { 686 + const record = { 687 + embed: { 688 + $type: 'app.bsky.embed.recordWithMedia', 689 + record: { record: { uri: 'at://did:plc:someone/app.bsky.feed.post/abc', cid: 'bafycid' } }, 690 + media: { $type: 'app.bsky.embed.images', images: [] }, 691 + }, 692 + } 693 + 694 + const result = parsePostEmbed(record, TEST_DID) 695 + assert.isNull(result) 696 + }) 697 + }) 698 + 699 + test.group('parsePostEmbed — no embed / malformed', () => { 700 + test('no embed field returns null', ({ assert }) => { 701 + const record = { 702 + $type: 'app.bsky.feed.post', 703 + text: 'just text', 704 + } 705 + 706 + const result = parsePostEmbed(record, TEST_DID) 707 + assert.isNull(result) 708 + }) 709 + 710 + test('malformed blob missing ref.$link returns null and warns', ({ assert }) => { 711 + const record = { 712 + embed: { 713 + $type: 'app.bsky.embed.images', 714 + images: [ 715 + { 716 + alt: 'broken', 717 + image: { 718 + $type: 'blob', 719 + // missing ref.$link 720 + mimeType: 'image/jpeg', 721 + size: 100, 722 + }, 723 + }, 724 + ], 725 + }, 726 + } 727 + 728 + const result = parsePostEmbed(record, TEST_DID) 729 + assert.isNull(result) 730 + }) 731 + 732 + test('non-object record returns null', ({ assert }) => { 733 + const result = parsePostEmbed('not an object', TEST_DID) 734 + assert.isNull(result) 735 + }) 736 + }) 737 + 738 + test.group('parseJetstreamEvent — post create with embeds integration', () => { 739 + test('post create with images embed populates event.embed', ({ assert }) => { 740 + const event = parseJetstreamEvent({ 741 + did: 'did:plc:author123', 742 + time_us: 1725911162329308, 743 + kind: 'commit', 744 + commit: { 745 + rev: 'rev1', 746 + operation: 'create', 747 + collection: 'app.bsky.feed.post', 748 + rkey: 'postrkeyimg', 749 + record: { 750 + $type: 'app.bsky.feed.post', 751 + text: 'Check out these pics', 752 + createdAt: '2024-09-09T19:48:00.000Z', 753 + embed: { 754 + $type: 'app.bsky.embed.images', 755 + images: [ 756 + { 757 + alt: 'test image', 758 + image: { 759 + $type: 'blob', 760 + ref: { $link: 'bafkreitestcid' }, 761 + mimeType: 'image/jpeg', 762 + size: 50000, 763 + }, 764 + }, 765 + ], 766 + }, 767 + }, 768 + cid: 'bafypostcidimg', 769 + }, 770 + }) 771 + 772 + assert.isNotNull(event) 773 + if (!event || event.kind !== 'post') return 774 + 775 + assert.isNotNull(event.embed) 776 + assert.equal(event.embed!.type, 'images') 777 + assert.isNull(event.embeddedPostUri) 778 + }) 779 + 780 + test('post create with no embed has null embed', ({ assert }) => { 781 + const event = parseJetstreamEvent(POST_CREATE_EVENT) 782 + 783 + assert.isNotNull(event) 784 + if (!event || event.kind !== 'post') return 785 + 786 + assert.isNull(event.embed) 787 + }) 788 + 789 + test('post create with record embed has null embed but non-null embeddedPostUri', ({ 790 + assert, 791 + }) => { 792 + const event = parseJetstreamEvent(POST_CREATE_WITH_EMBED_RECORD_EVENT) 793 + 794 + assert.isNotNull(event) 795 + if (!event || event.kind !== 'post') return 796 + 797 + assert.isNull(event.embed) 798 + assert.isNotNull(event.embeddedPostUri) 799 + }) 800 + })
+136 -10
tests/unit/services/jetstream_consumer.spec.ts
··· 180 180 } 181 181 } 182 182 183 + function makePostEventWithImagesEmbed(authorDid: string, timeUs = 1725911162329308) { 184 + return { 185 + did: authorDid, 186 + time_us: timeUs, 187 + kind: 'commit', 188 + commit: { 189 + rev: 'rev4img', 190 + operation: 'create', 191 + collection: 'app.bsky.feed.post', 192 + rkey: 'postrkeyimgembed', 193 + record: { 194 + $type: 'app.bsky.feed.post', 195 + text: 'Post with images', 196 + createdAt: '2024-09-09T19:48:00.000Z', 197 + embed: { 198 + $type: 'app.bsky.embed.images', 199 + images: [ 200 + { 201 + alt: 'test image', 202 + aspectRatio: { width: 800, height: 600 }, 203 + image: { 204 + $type: 'blob', 205 + ref: { $link: 'bafkreitestcid123' }, 206 + mimeType: 'image/jpeg', 207 + size: 50000, 208 + }, 209 + }, 210 + ], 211 + }, 212 + }, 213 + cid: 'bafypostcidimg', 214 + }, 215 + } 216 + } 217 + 183 218 function makeQuotePostEvent( 184 219 quoterDid: string, 185 220 embeddedPostUri: string, ··· 739 774 ) 740 775 741 776 test.group( 742 - 'JetstreamConsumer — quote post by tracked author embedding tracked author → snapshot AND quote', 777 + 'JetstreamConsumer — quote post by tracked author embedding tracked author → no snapshot, quote engagement only', 743 778 () => { 744 - test('tracked author quoting tracked author fires both snapshot insert and quote engagement', async ({ 779 + test('tracked author quoting tracked author skips snapshot but fires quote engagement', async ({ 745 780 assert, 746 781 }) => { 747 782 const secondTrackedDid = 'did:plc:secondtracked789' ··· 758 793 759 794 await consumer.flushBuffer() 760 795 761 - // Snapshot for the quoter's own post 762 - assert.equal(store.snapshotInsertCalls.length, 1) 763 - assert.equal(store.snapshotInsertCalls[0][0].postAuthorDid, TRACKED_AUTHOR_DID) 796 + // No snapshot — quote posts are skipped in Part A 797 + assert.equal(store.snapshotInsertCalls.length, 0) 764 798 765 799 // Quote engagement event for the embedded author's post 766 800 assert.equal(store.insertCalls.length, 1) ··· 777 811 ) 778 812 779 813 test.group( 780 - 'JetstreamConsumer — quote post embedding untracked author → no quote engagement', 814 + 'JetstreamConsumer — quote post embedding untracked author → no snapshot, no quote engagement', 781 815 () => { 782 - test('quoting an untracked author produces no engagement event', async ({ assert }) => { 816 + test('quoting an untracked author produces no snapshot and no engagement event', async ({ 817 + assert, 818 + }) => { 783 819 const embeddedPostUri = `at://${UNTRACKED_AUTHOR_DID}/app.bsky.feed.post/postrkeyzzz` 784 820 785 821 const { fakeWs, store, consumer } = makeConsumer() 786 822 787 823 await startConsumerInBackground(consumer) 788 824 789 - // Tracked author quotes an untracked author — no quote event 825 + // Tracked author quotes an untracked author — no quote event, no snapshot 790 826 fakeWs.emit(makeQuotePostEvent(TRACKED_AUTHOR_DID, embeddedPostUri)) 791 827 792 828 await consumer.flushBuffer() 793 829 794 - // Snapshot for the tracked author's own post (since they ARE tracked) 795 - assert.equal(store.snapshotInsertCalls.length, 1) 830 + // No snapshot — quote posts are skipped in Part A 831 + assert.equal(store.snapshotInsertCalls.length, 0) 796 832 797 833 // No quote engagement event (embedded author is untracked) 798 834 assert.equal(store.insertCalls.length, 0) ··· 985 1021 986 1022 // Still only 1 insert call from before 987 1023 assert.equal(store.insertCalls.length, 1) 1024 + 1025 + await consumer.shutdown() 1026 + }) 1027 + } 1028 + ) 1029 + 1030 + // --------------------------------------------------------------------------- 1031 + // Post embed tests 1032 + // --------------------------------------------------------------------------- 1033 + 1034 + test.group( 1035 + 'JetstreamConsumer — tracked-author post with images embed → snapshot has embed populated', 1036 + () => { 1037 + test('images embed is carried through to the snapshot', async ({ assert }) => { 1038 + const { fakeWs, store, consumer } = makeConsumer() 1039 + 1040 + await startConsumerInBackground(consumer) 1041 + 1042 + fakeWs.emit(makePostEventWithImagesEmbed(TRACKED_AUTHOR_DID)) 1043 + 1044 + await consumer.flushBuffer() 1045 + 1046 + assert.equal(store.snapshotInsertCalls.length, 1) 1047 + const snapshot = store.snapshotInsertCalls[0][0] 1048 + assert.isNotNull(snapshot.embed) 1049 + assert.equal(snapshot.embed!.type, 'images') 1050 + if (snapshot.embed?.type === 'images') { 1051 + assert.equal(snapshot.embed.items.length, 1) 1052 + assert.include(snapshot.embed.items[0].thumb, 'bafkreitestcid123') 1053 + assert.include(snapshot.embed.items[0].fullsize, 'bafkreitestcid123') 1054 + assert.equal(snapshot.embed.items[0].alt, 'test image') 1055 + assert.deepEqual(snapshot.embed.items[0].aspectRatio, { width: 800, height: 600 }) 1056 + } 1057 + 1058 + await consumer.shutdown() 1059 + }) 1060 + } 1061 + ) 1062 + 1063 + test.group( 1064 + 'JetstreamConsumer — tracked-author quote post → no snapshot, cursor not advanced in Part A', 1065 + () => { 1066 + test('quote post by tracked author does NOT buffer a snapshot, but Part B still fires for tracked embedded author', async ({ 1067 + assert, 1068 + }) => { 1069 + const secondTrackedDid = 'did:plc:secondtracked789' 1070 + const embeddedPostUri = `at://${secondTrackedDid}/app.bsky.feed.post/postrkeydef` 1071 + 1072 + const { fakeWs, store, consumer, writeCursorCalls } = makeConsumer({ 1073 + trackedDids: new Set([TRACKED_AUTHOR_DID, secondTrackedDid]), 1074 + }) 1075 + 1076 + await startConsumerInBackground(consumer) 1077 + 1078 + fakeWs.emit(makeQuotePostEvent(TRACKED_AUTHOR_DID, embeddedPostUri)) 1079 + 1080 + await consumer.flushBuffer() 1081 + 1082 + // No snapshot buffered (quote post skipped in Part A) 1083 + assert.equal(store.snapshotInsertCalls.length, 0) 1084 + 1085 + // Part B fires: quote engagement event for the embedded author 1086 + assert.equal(store.insertCalls.length, 1) 1087 + assert.equal(store.insertCalls[0][0].kind, 'quote') 1088 + assert.equal(store.insertCalls[0][0].postUri, embeddedPostUri) 1089 + assert.equal(store.insertCalls[0][0].postAuthorDid, secondTrackedDid) 1090 + 1091 + // Cursor IS advanced by Part B (since it wrote an engagement event) 1092 + assert.isAbove(writeCursorCalls.length, 0) 1093 + 1094 + await consumer.shutdown() 1095 + }) 1096 + } 1097 + ) 1098 + 1099 + test.group( 1100 + 'JetstreamConsumer — tracked-author post with no embed → snapshot has embed: null', 1101 + () => { 1102 + test('plain text post snapshot has null embed', async ({ assert }) => { 1103 + const { fakeWs, store, consumer } = makeConsumer() 1104 + 1105 + await startConsumerInBackground(consumer) 1106 + 1107 + fakeWs.emit(makePostEvent(TRACKED_AUTHOR_DID)) 1108 + 1109 + await consumer.flushBuffer() 1110 + 1111 + assert.equal(store.snapshotInsertCalls.length, 1) 1112 + const snapshot = store.snapshotInsertCalls[0][0] 1113 + assert.isNull(snapshot.embed) 988 1114 989 1115 await consumer.shutdown() 990 1116 })