A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

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

Pre-flight the post-length guard so an over-limit publish can't orphan a document

The 300-grapheme backstop lived inside buildBskyPost, which publish() calls
at step 2 — after step 1 has already created the document record. If it fired,
the PDS kept a published-but-postless document (and a retry minted a second
orphan). Hoist the check into a shared assertBskyPostWithinLimit() and run it
in publish() before any record is written; buildBskyPost keeps calling it as a
final backstop. Adds a regression test asserting an over-limit publish rejects
with zero records written.

+55 -7
+20
src/lib/publish/publisher.test.ts
··· 527 527 ] ); 528 528 expect( docWrites.at( -1 )!.record.bskyPostRef ).toBeDefined(); 529 529 } ); 530 + 531 + it( 'rejects an over-limit post before writing ANY record (no orphan document)', async () => { 532 + const { agent, created } = fakeAgent(); 533 + await expect( 534 + publish( 535 + agent, 536 + { did: 'did:plc:me', handle: 'me.bsky.social' }, 537 + { 538 + title: 'Hello', 539 + description: 'x'.repeat( 320 ), 540 + blocks: BLOCKS, 541 + publicationUri: 'at://did:plc:me/site.standard.publication/pub', 542 + publicationCid: 'cidpub', 543 + publicationSlug: 'pub', 544 + } 545 + ) 546 + ).rejects.toThrow( /300/ ); 547 + // The grapheme guard must fire pre-flight: nothing was written to the PDS. 548 + expect( created ).toHaveLength( 0 ); 549 + } ); 530 550 } );
+6
src/lib/publish/publisher.ts
··· 3 3 import { 4 4 buildDocumentRecord, 5 5 buildBskyPost, 6 + assertBskyPostWithinLimit, 6 7 canonicalArticleUrl, 7 8 type StrongRef, 8 9 } from './records'; ··· 86 87 const mentions = collectMentions( input.blocks ); 87 88 // The post BODY uses the writer-typed lede only (never the derived excerpt). 88 89 const bodyLede = input.description?.trim() || undefined; 90 + 91 + // Pre-flight the companion post's length BEFORE writing anything: if it's over the 92 + // grapheme limit, fail here rather than after step 1 has already created an orphan 93 + // document with no post (buildBskyPost repeats this check as a backstop). 94 + assertBskyPostWithinLimit( { title: input.title, articleUrl, bodyLede, mentions } ); 89 95 90 96 // 1. Document first (no bskyPostRef yet) so the post can embed its strongRef. 91 97 const docRes = await agent.com.atproto.repo.createRecord( {
+29 -7
src/lib/publish/records.ts
··· 268 268 } 269 269 270 270 /** Bluesky rejects posts longer than this many graphemes. */ 271 - const BSKY_POST_MAX_GRAPHEMES = 300; 271 + export const BSKY_POST_MAX_GRAPHEMES = 300; 272 + 273 + /** 274 + * Throw if the companion post would exceed Bluesky's grapheme limit. Call this as a 275 + * PRE-FLIGHT in `publish()` (before any record is written) so the guard can't leave an 276 + * orphaned document; `buildBskyPost` also calls it as a final backstop. Inputs match 277 + * `assemblePostText`, so the assembled text is identical to what gets published. 278 + */ 279 + export function assertBskyPostWithinLimit( input: { 280 + title: string; 281 + articleUrl: string; 282 + bodyLede?: string; 283 + mentions?: Mention[]; 284 + } ): void { 285 + const { text } = assemblePostText( input ); 286 + const length = graphemeLength( text ); 287 + if ( length > BSKY_POST_MAX_GRAPHEMES ) { 288 + throw new Error( 289 + `Bluesky post is ${ length } characters; the limit is ${ BSKY_POST_MAX_GRAPHEMES }. Shorten the subtitle or remove a mention.` 290 + ); 291 + } 292 + } 272 293 273 294 /** 274 295 * Build the companion Bluesky post (Decision 0005). Body text + facets come from ··· 299 320 mentions: input.mentions, 300 321 } ); 301 322 302 - const length = graphemeLength( text ); 303 - if ( length > BSKY_POST_MAX_GRAPHEMES ) { 304 - throw new Error( 305 - `Bluesky post is ${ length } characters; the limit is ${ BSKY_POST_MAX_GRAPHEMES }. Shorten the subtitle or remove a mention.` 306 - ); 307 - } 323 + // Backstop: `publish()` already pre-flights this before writing any record. 324 + assertBskyPostWithinLimit( { 325 + title: input.title, 326 + articleUrl: input.articleUrl, 327 + bodyLede: input.bodyLede, 328 + mentions: input.mentions, 329 + } ); 308 330 309 331 return { 310 332 $type: 'app.bsky.feed.post',