@atproto/bsky 0.3.12 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -1
  3. package/dist/api/app/bsky/notification/getGroupedNotifications.js +14 -3
  4. package/dist/api/app/bsky/notification/getGroupedNotifications.js.map +1 -1
  5. package/dist/api/blob-resolver.d.ts.map +1 -1
  6. package/dist/api/blob-resolver.js +9 -8
  7. package/dist/api/blob-resolver.js.map +1 -1
  8. package/dist/config.d.ts +2 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +7 -6
  11. package/dist/config.js.map +1 -1
  12. package/dist/hydration/external.d.ts +52 -45
  13. package/dist/hydration/external.d.ts.map +1 -1
  14. package/dist/hydration/external.js +132 -114
  15. package/dist/hydration/external.js.map +1 -1
  16. package/dist/hydration/hydrator.d.ts +10 -8
  17. package/dist/hydration/hydrator.d.ts.map +1 -1
  18. package/dist/hydration/hydrator.js +82 -189
  19. package/dist/hydration/hydrator.js.map +1 -1
  20. package/dist/image/server.d.ts.map +1 -1
  21. package/dist/image/server.js +12 -1
  22. package/dist/image/server.js.map +1 -1
  23. package/dist/lexicons/tools/ozone/moderation/emitEvent.defs.d.ts +2 -2
  24. package/dist/lexicons/tools/ozone/moderation/emitEvent.defs.js.map +1 -1
  25. package/dist/lexicons/tools/ozone/report/closeReports.defs.d.ts +1 -1
  26. package/dist/lexicons/tools/ozone/report/closeReports.defs.d.ts.map +1 -1
  27. package/dist/lexicons/tools/ozone/report/closeReports.defs.js +1 -1
  28. package/dist/lexicons/tools/ozone/report/closeReports.defs.js.map +1 -1
  29. package/dist/telemetry/events.d.ts +11 -4
  30. package/dist/telemetry/events.d.ts.map +1 -1
  31. package/dist/telemetry/events.js +29 -5
  32. package/dist/telemetry/events.js.map +1 -1
  33. package/dist/util/external/standard-site.d.ts +2 -3
  34. package/dist/util/external/standard-site.d.ts.map +1 -1
  35. package/dist/util/external/standard-site.js +2 -3
  36. package/dist/util/external/standard-site.js.map +1 -1
  37. package/dist/util/uris.d.ts +5 -2
  38. package/dist/util/uris.d.ts.map +1 -1
  39. package/dist/util/uris.js +14 -1
  40. package/dist/util/uris.js.map +1 -1
  41. package/dist/views/index.d.ts +2 -2
  42. package/dist/views/index.d.ts.map +1 -1
  43. package/dist/views/index.js +17 -11
  44. package/dist/views/index.js.map +1 -1
  45. package/package.json +11 -12
@@ -6,12 +6,11 @@ import { NOTIFICATION_REASON } from '../api/app/bsky/notification/constants.js';
6
6
  import { app, chat, com, place, site, social } from '../lexicons/index.js';
7
7
  import { hydrationLogger } from '../logger.js';
8
8
  import { events } from '../telemetry/events.js';
9
- import { SITE_STANDARD_NSID_PREFIX } from '../util/external/standard-site.js';
10
- import { uriToDid, uriToDid as didFromUri } from '../util/uris.js';
9
+ import { recordUriOf, uriToDid, uriToDid as didFromUri } from '../util/uris.js';
11
10
  import { MAX_PROFILE_LINKS } from '../views/profile-link-rules.js';
12
- import { isExternalEmbedType, isListRuleType, isRecordEmbedType, isRecordWithMediaType, } from '../views/types.js';
11
+ import { isListRuleType, isRecordEmbedType, isRecordWithMediaType, } from '../views/types.js';
13
12
  import { ActorHydrator, } from './actor.js';
14
- import { ExternalHydrator, actionExternalRecordTakedowns, genericRecordKey, parseGenericRecordKey, trackExternalHydration, } from './external.js';
13
+ import { EXTERNAL_HYDRATION_LIMITS, ExternalHydrator, MAX_CONCURRENT_BACKLINK_REQUESTS, MAX_EXTERNAL_HYDRATION_PASSES, actionExternalRecordTakedowns, externalAssociatedRefsFromPosts, genericRecordKey, trackExternalHydration, } from './external.js';
15
14
  import { FeedHydrator, } from './feed.js';
16
15
  import { GraphHydrator, } from './graph.js';
17
16
  import { LabelHydrator, Labels, } from './label.js';
@@ -424,13 +423,7 @@ export class Hydrator {
424
423
  postUrisWithPostgates.add(uri);
425
424
  }
426
425
  }
427
- const siteStandardRefs = siteStandardRefsFromPosts(postsLayer0, postsLayer1, postsLayer2);
428
- const siteStandardLabelSubjects = dedupeStrs(siteStandardRefs.map((ref) => ref.uri));
429
- // Site-standard record owners need profile basics so we can populate
430
- // `associatedProfiles` on the external embed view. Include them in the
431
- // post-author profile fetch below; any DIDs surfaced later by the
432
- // dataplane are picked up by the top-up after the parallel batch.
433
- const ssRefDids = siteStandardRefs.map((ref) => uriToDid(ref.uri));
426
+ const externalRefs = externalAssociatedRefsFromPosts(postsLayer0, postsLayer1, postsLayer2);
434
427
  // Collect known-liker profiles and author-liker block relationships.
435
428
  const knownLikers = await knownLikersPromise;
436
429
  const knownLikerDids = [];
@@ -446,15 +439,14 @@ export class Hydrator {
446
439
  });
447
440
  const knownProfileDids = dedupeStrs([
448
441
  ...allPostUris.map(didFromUri),
449
- ...ssRefDids,
450
442
  ...knownLikerDids,
451
443
  ]);
452
- const [postAggs, postViewers, labels, { postBlocks, additionalRelationshipsBidirectionalBlocks: authorsAndKnownLikersBidirectionalBlocks, }, profileState, listState, feedGenState, labelerState, starterPackState, postgates, { documents: siteStandardDocuments, publications: siteStandardPublications, },] = await Promise.all([
444
+ const [postAggs, postViewers, labels, { postBlocks, additionalRelationshipsBidirectionalBlocks: authorsAndKnownLikersBidirectionalBlocks, }, profileState, listState, feedGenState, labelerState, starterPackState, postgates, externalState,] = await Promise.all([
453
445
  this.feed.getPostAggregates(allRefs, ctx.viewer),
454
446
  ctx.viewer
455
447
  ? this.feed.getPostViewerStates(threadRefs, ctx.viewer)
456
448
  : undefined,
457
- this.label.getLabelsForSubjects([...allPostUris, ...siteStandardLabelSubjects], ctx.labelers),
449
+ this.label.getLabelsForSubjects(allPostUris, ctx.labelers),
458
450
  this.hydratePostBlocks(posts, ctx, authorsAndKnownLikersRelationships),
459
451
  this.hydrateProfiles(knownProfileDids, ctx),
460
452
  this.hydrateLists([...nestedListUris, ...threadgateListUris], ctx),
@@ -462,29 +454,15 @@ export class Hydrator {
462
454
  this.hydrateLabelers(nestedLabelerDids, ctx),
463
455
  this.hydrateStarterPacksBasic(nestedStarterPackUris, ctx),
464
456
  this.feed.getPostgatesForPosts([...postUrisWithPostgates.values()]),
465
- this.external.getSiteStandardRecordsByRef(siteStandardRefs, ctx.includeTakedowns),
457
+ this.hydrateEmbedExternalViewFromRefs(externalRefs, ctx, {
458
+ hydration: 'basic',
459
+ }),
466
460
  ]);
467
461
  if (!ctx.includeTakedowns) {
468
462
  actionTakedownLabels(allPostUris, posts, labels);
469
- actionSiteStandardTakedownLabels(siteStandardDocuments, siteStandardPublications, labels);
470
- }
471
- // Defensive top-up: in the unlikely case the dataplane returned a
472
- // publication owned by a DID not present in the post-author or
473
- // pinned-ref sets, fetch its profile serially and fold it in.
474
- const knownProfileDidsSet = new Set(knownProfileDids);
475
- const extraSsDids = [];
476
- for (const key of siteStandardPublications.keys()) {
477
- const did = uriToDid(parseGenericRecordKey(key).uri);
478
- if (!knownProfileDidsSet.has(did)) {
479
- knownProfileDidsSet.add(did);
480
- extraSsDids.push(did);
481
- }
482
463
  }
483
- const mergedProfileState = extraSsDids.length
484
- ? mergeStates(profileState, await this.hydrateProfilesBasic(extraSsDids, ctx))
485
- : profileState;
486
464
  // combine all hydration state
487
- return mergeManyStates(mergedProfileState, listState, feedGenState, labelerState, starterPackState, {
465
+ return mergeManyStates(externalState, profileState, listState, feedGenState, labelerState, starterPackState, {
488
466
  posts,
489
467
  postAggs,
490
468
  postViewers,
@@ -494,8 +472,6 @@ export class Hydrator {
494
472
  labels,
495
473
  threadgates,
496
474
  postgates,
497
- siteStandardDocuments,
498
- siteStandardPublications,
499
475
  ctx,
500
476
  });
501
477
  }
@@ -626,20 +602,23 @@ export class Hydrator {
626
602
  * generic batch within the traversal (the root batch is pass 1); nested
627
603
  * batches continue the count. Nested passes share the caller's
628
604
  * `hydrationMetrics` tracker; without one, this call owns the traversal and
629
- * reports its totals.
605
+ * reports its totals. `options.hydration` (default `'detailed'`) applies to
606
+ * the whole traversal and is propagated to every nested pass.
630
607
  */
631
- async hydrateEmbedExternalViewFromUris(uris, ctx, seenRecordKeys = new Set(), hydrationPass = 1, hydrationMetrics) {
632
- return trackExternalHydration('uris', ctx.xrpcEndpoint, hydrationMetrics, (hydrationMetrics) => this.hydrateEmbedExternalView(uris.map(uriToRef), ctx, seenRecordKeys, hydrationPass, hydrationMetrics));
608
+ async hydrateEmbedExternalViewFromUris(uris, ctx, options = {}) {
609
+ const { hydration = 'detailed', seenRecordKeys = new Set(), hydrationPass = 1, hydrationMetrics, } = options;
610
+ return trackExternalHydration('uris', ctx.xrpcEndpoint, hydrationMetrics, (hydrationMetrics) => this.hydrateEmbedExternalView(uris.map(uriToRef), ctx, hydration, seenRecordKeys, hydrationPass, hydrationMetrics));
633
611
  }
634
612
  /**
635
613
  * Like `hydrateEmbedExternalViewFromUris`, but for exact record versions
636
614
  * pinned by strong refs. Roots land only in `externalRecordsByRef`; their
637
615
  * URI dependencies are hydrated as latest versions by the URI path.
638
616
  */
639
- async hydrateEmbedExternalViewFromRefs(refs, ctx, seenRecordKeys = new Set(), hydrationPass = 1, hydrationMetrics) {
640
- return trackExternalHydration('refs', ctx.xrpcEndpoint, hydrationMetrics, (hydrationMetrics) => this.hydrateEmbedExternalView(refs, ctx, seenRecordKeys, hydrationPass, hydrationMetrics));
617
+ async hydrateEmbedExternalViewFromRefs(refs, ctx, options = {}) {
618
+ const { hydration = 'detailed', seenRecordKeys = new Set(), hydrationPass = 1, hydrationMetrics, } = options;
619
+ return trackExternalHydration('refs', ctx.xrpcEndpoint, hydrationMetrics, (hydrationMetrics) => this.hydrateEmbedExternalView(refs, ctx, hydration, seenRecordKeys, hydrationPass, hydrationMetrics));
641
620
  }
642
- async hydrateEmbedExternalView(refs, ctx, seenRecordKeys, hydrationPass, hydrationMetrics) {
621
+ async hydrateEmbedExternalView(refs, ctx, hydration, seenRecordKeys, hydrationPass, hydrationMetrics) {
643
622
  // Filter at the entry point so each lookup runs at most once per shared
644
623
  // traversal, even for callers that did not prefilter their input. Null or
645
624
  // unavailable results stay seen as completed lookups. Nothing unseen means
@@ -699,7 +678,12 @@ export class Hydrator {
699
678
  });
700
679
  actionExternalRecordTakedowns(baseState, ctx.includeTakedowns);
701
680
  // Discover and hydrate any additional view dependencies
702
- const depsState = await this.hydrateExternalViewDependencies(baseState, items, ctx, seenRecordKeys, hydrationPass, hydrationMetrics);
681
+ const depsState = await this.hydrateExternalViewDependencies(baseState, items, ctx, {
682
+ hydration,
683
+ seenRecordKeys,
684
+ hydrationPass,
685
+ hydrationMetrics,
686
+ });
703
687
  // A supplemental latest lookup returning the same URI and CID is the same
704
688
  // version, so it may replace an unavailable exact entry; other CIDs have
705
689
  // other keys and never satisfy a pin.
@@ -718,13 +702,18 @@ export class Hydrator {
718
702
  * `hydrationPass` is the number of the generic batch that produced `state`
719
703
  * (1-based, default 1 for prehydrated state, which is treated as the root
720
704
  * batch); the next nested batch is `hydrationPass + 1`. `hydrationMetrics`
721
- * is shared as in `hydrateEmbedExternalViewFromUris`.
705
+ * is shared as in `hydrateEmbedExternalViewFromUris`. `options.hydration`
706
+ * (default `'detailed'`) decides, per collection, whether social-preview
707
+ * samples are scheduled; backlink counts and content dependencies are
708
+ * discovered at both levels.
722
709
  */
723
- async hydrateExternalViewDependencies(state, refs, ctx, seenRecordKeys = new Set(), hydrationPass = 1, hydrationMetrics) {
724
- return trackExternalHydration('dependencies', ctx.xrpcEndpoint, hydrationMetrics, (hydrationMetrics) => this.discoverExternalViewDependencies(state, refs, ctx, seenRecordKeys, hydrationPass, hydrationMetrics));
710
+ async hydrateExternalViewDependencies(state, refs, ctx, options = {}) {
711
+ const { hydration = 'detailed', seenRecordKeys = new Set(), hydrationPass = 1, hydrationMetrics, } = options;
712
+ return trackExternalHydration('dependencies', ctx.xrpcEndpoint, hydrationMetrics, (hydrationMetrics) => this.discoverExternalViewDependencies(state, refs, ctx, hydration, seenRecordKeys, hydrationPass, hydrationMetrics));
725
713
  }
726
- async discoverExternalViewDependencies(state, refs, ctx, seenRecordKeys, hydrationPass, hydrationMetrics) {
727
- // Backlink sources to sample per target, each with its own bound.
714
+ async discoverExternalViewDependencies(state, refs, ctx, hydration, seenRecordKeys, hydrationPass, hydrationMetrics) {
715
+ // Social-preview backlink sources to sample per target, each with its own
716
+ // bound. A target with no samples still gets its aggregate count.
728
717
  const targets = new Map();
729
718
  const discoveredUris = new Set();
730
719
  const rootBatch = !hydrationMetrics.rootsCounted;
@@ -756,7 +745,8 @@ export class Hydrator {
756
745
  if (!parsed.success ||
757
746
  !isDidString(parsed.value.authority) ||
758
747
  !parsed.value.rkey ||
759
- parsed.value.hash) {
748
+ parsed.value.query !== undefined ||
749
+ parsed.value.hash !== undefined) {
760
750
  continue;
761
751
  }
762
752
  /*
@@ -765,24 +755,30 @@ export class Hydrator {
765
755
  const { collection } = parsed.value;
766
756
  if (collection === site.standard.document.$type &&
767
757
  site.standard.document.$matches(record, { strict: false })) {
768
- targets.set(uri, [
769
- {
770
- collection: site.standard.graph.recommend.$type,
771
- limit: ExternalHydrator.MAX_BACKLINK_PREVIEWS,
772
- },
773
- ]);
758
+ const limits = EXTERNAL_HYDRATION_LIMITS[site.standard.document.$type];
759
+ targets.set(uri, hydration === 'detailed'
760
+ ? [
761
+ {
762
+ collection: site.standard.graph.recommend.$type,
763
+ limit: limits.recommendationPreviews,
764
+ },
765
+ ]
766
+ : []);
774
767
  const publicationUri = recordUriOf(record.site, site.standard.publication.$type);
775
768
  if (publicationUri)
776
769
  discoveredUris.add(publicationUri);
777
770
  }
778
771
  else if (collection === site.standard.publication.$type &&
779
772
  site.standard.publication.$matches(record, { strict: false })) {
780
- targets.set(uri, [
781
- {
782
- collection: site.standard.graph.subscription.$type,
783
- limit: ExternalHydrator.MAX_BACKLINK_PREVIEWS,
784
- },
785
- ]);
773
+ const limits = EXTERNAL_HYDRATION_LIMITS[site.standard.publication.$type];
774
+ targets.set(uri, hydration === 'detailed'
775
+ ? [
776
+ {
777
+ collection: site.standard.graph.subscription.$type,
778
+ limit: limits.subscriberPreviews,
779
+ },
780
+ ]
781
+ : []);
786
782
  }
787
783
  else if (collection === place.stream.livestream.$type &&
788
784
  place.stream.livestream.$matches(record, { strict: false })) {
@@ -790,16 +786,20 @@ export class Hydrator {
790
786
  }
791
787
  else if (collection === social.grain.gallery.$type &&
792
788
  social.grain.gallery.$matches(record, { strict: false })) {
793
- targets.set(uri, [
794
- {
789
+ const limits = EXTERNAL_HYDRATION_LIMITS[social.grain.gallery.$type];
790
+ // Membership items are content, sampled at every level.
791
+ const samples = [];
792
+ if (hydration === 'detailed') {
793
+ samples.push({
795
794
  collection: social.grain.favorite.$type,
796
- limit: ExternalHydrator.MAX_BACKLINK_PREVIEWS,
797
- },
798
- {
799
- collection: social.grain.gallery.item.$type,
800
- limit: ExternalHydrator.MAX_BACKLINK_GALLERY_ITEMS,
801
- },
802
- ]);
795
+ limit: limits.favoritePreviews,
796
+ });
797
+ }
798
+ samples.push({
799
+ collection: social.grain.gallery.item.$type,
800
+ limit: limits.items,
801
+ });
802
+ targets.set(uri, samples);
803
803
  }
804
804
  else if (collection === social.grain.gallery.item.$type &&
805
805
  social.grain.gallery.item.$matches(record, { strict: false })) {
@@ -826,7 +826,7 @@ export class Hydrator {
826
826
  }),
827
827
  (async () => {
828
828
  // @NOTE Bound fan-out as well as each sample; counts are independent.
829
- for (const batch of chunkArray(queries, ExternalHydrator.MAX_BACKLINK_FANOUT)) {
829
+ for (const batch of chunkArray(queries, MAX_CONCURRENT_BACKLINK_REQUESTS)) {
830
830
  await Promise.all(batch.map(async ({ uri, collection, limit }) => {
831
831
  let page;
832
832
  try {
@@ -838,9 +838,11 @@ export class Hydrator {
838
838
  return;
839
839
  }
840
840
  const links = page.backlinks
841
- .filter((b) => parseAtUriString(b.uri).success)
842
- .slice(0, limit)
843
- .map((b) => b.uri);
841
+ .flatMap(({ uri }) => {
842
+ const link = recordUriOf(uri, collection);
843
+ return link ? [link] : [];
844
+ })
845
+ .slice(0, limit);
844
846
  backlinks.set(uri, dedupeStrs([...(backlinks.get(uri) ?? []), ...links]));
845
847
  links.forEach((link) => discoveredUris.add(link));
846
848
  }));
@@ -852,20 +854,24 @@ export class Hydrator {
852
854
  }
853
855
  const newUris = [...discoveredUris].filter((uri) => !seenRecordKeys.has(genericRecordKey({ uri })));
854
856
  let nested = {};
855
- if (newUris.length &&
856
- hydrationPass >= ExternalHydrator.MAX_EXTERNAL_HYDRATION_PASSES) {
857
+ if (newUris.length && hydrationPass >= MAX_EXTERNAL_HYDRATION_PASSES) {
857
858
  // Keep the partial state. Skipped URIs are not marked seen: they were
858
859
  // never fetched.
859
860
  hydrationMetrics.capped = true;
860
861
  hydrationLogger.warn({
861
862
  hydrationPass,
862
- maxPasses: ExternalHydrator.MAX_EXTERNAL_HYDRATION_PASSES,
863
+ maxPasses: MAX_EXTERNAL_HYDRATION_PASSES,
863
864
  skipped: newUris.length,
864
865
  }, 'external hydration pass limit reached, skipping dependencies');
865
866
  }
866
867
  else if (newUris.length) {
867
868
  try {
868
- nested = await this.hydrateEmbedExternalViewFromUris(newUris, ctx, seenRecordKeys, hydrationPass + 1, hydrationMetrics);
869
+ nested = await this.hydrateEmbedExternalViewFromUris(newUris, ctx, {
870
+ hydration,
871
+ seenRecordKeys,
872
+ hydrationPass: hydrationPass + 1,
873
+ hydrationMetrics,
874
+ });
869
875
  }
870
876
  catch (err) {
871
877
  // Skip the whole batch; partial child state could miss moderation.
@@ -1573,52 +1579,6 @@ const nestedRecordUris = (post) => {
1573
1579
  }
1574
1580
  return uris;
1575
1581
  };
1576
- /**
1577
- * Returns every strongRef the post embedded as `external.associatedRefs`,
1578
- * regardless of collection. Callers are responsible for filtering to NSIDs
1579
- * they care about.
1580
- */
1581
- const externalAssociatedRefs = (post) => {
1582
- const embed = post?.embed;
1583
- if (!embed)
1584
- return [];
1585
- if (isExternalEmbedType(embed)) {
1586
- return embed.external.associatedRefs ?? [];
1587
- }
1588
- if (isRecordWithMediaType(embed) && isExternalEmbedType(embed.media)) {
1589
- return embed.media.external.associatedRefs ?? [];
1590
- }
1591
- return [];
1592
- };
1593
- /**
1594
- * Collects standard.site refs across all post layers, deduped by `uri+cid` so
1595
- * the dataplane batch is minimal even if multiple posts in the layer reference
1596
- * the same exact version of an SS record. The same `${uri}@${cid}` keys are
1597
- * what `getSiteStandardRecordsByRef` uses on the resulting hydration maps,
1598
- * which lets `lookupAssociatedSiteStandardRecords` do an O(1) version-exact
1599
- * lookup at view time.
1600
- */
1601
- const siteStandardRefsFromPosts = (...postLayers) => {
1602
- const seen = new Set();
1603
- const out = [];
1604
- for (const layer of postLayers) {
1605
- for (const item of layer.values()) {
1606
- if (!item)
1607
- continue;
1608
- for (const ref of externalAssociatedRefs(item.record)) {
1609
- if (!new AtUri(ref.uri).collection.startsWith(SITE_STANDARD_NSID_PREFIX)) {
1610
- continue;
1611
- }
1612
- const key = `${ref.uri}@${ref.cid}`;
1613
- if (seen.has(key))
1614
- continue;
1615
- seen.add(key);
1616
- out.push({ uri: ref.uri, cid: ref.cid });
1617
- }
1618
- }
1619
- }
1620
- return out;
1621
- };
1622
1582
  const getListUrisFromThreadgates = (gates) => {
1623
1583
  const uris = [];
1624
1584
  for (const gate of gates.values()) {
@@ -1708,8 +1668,6 @@ export const mergeStates = (stateA, stateB) => {
1708
1668
  externalRecordsByRef: mergeMaps(stateA.externalRecordsByRef, stateB.externalRecordsByRef),
1709
1669
  externalRecordBacklinks: mergeMaps(stateA.externalRecordBacklinks, stateB.externalRecordBacklinks),
1710
1670
  externalRecordBacklinkCounts: mergeMaps(stateA.externalRecordBacklinkCounts, stateB.externalRecordBacklinkCounts),
1711
- siteStandardDocuments: mergeMaps(stateA.siteStandardDocuments, stateB.siteStandardDocuments),
1712
- siteStandardPublications: mergeMaps(stateA.siteStandardPublications, stateB.siteStandardPublications),
1713
1671
  };
1714
1672
  };
1715
1673
  export const mergeManyStates = (...states) => {
@@ -1722,71 +1680,6 @@ const actionTakedownLabels = (keys, hydrationMap, labels) => {
1722
1680
  }
1723
1681
  }
1724
1682
  };
1725
- /**
1726
- * The URI when an externally authored reference is a complete DID-based
1727
- * record URI in `collection`, with a record key and no fragment.
1728
- */
1729
- const recordUriOf = (value, collection) => {
1730
- const parsed = parseAtUriString(value);
1731
- return parsed.success &&
1732
- isDidString(parsed.value.authority) &&
1733
- parsed.value.collection === collection &&
1734
- parsed.value.rkey &&
1735
- !parsed.value.hash
1736
- ? value
1737
- : undefined;
1738
- };
1739
- /**
1740
- * Apply takedown labels to the site.standard hydration maps. Per-record
1741
- * takedowns null any entry whose subject URI is taken down; pair takedowns
1742
- * propagate that null across doc/publication pairs so we never render half
1743
- * of a moderated embed. Pairs are discovered via `doc.record.site`; orphan
1744
- * docs and orphan publications are subject only to the per-record sweep.
1745
- */
1746
- const actionSiteStandardTakedownLabels = (documents, publications, labels) => {
1747
- // Pairings have to be captured before nulling — the doc record carries
1748
- // the publication URI in its `site` field, and we lose it once we null.
1749
- const pairings = [];
1750
- if (documents.size > 0 && publications.size > 0) {
1751
- const pubKeysByUri = new Map();
1752
- for (const key of publications.keys()) {
1753
- const { uri } = parseGenericRecordKey(key);
1754
- const list = pubKeysByUri.get(uri);
1755
- if (list)
1756
- list.push(key);
1757
- else
1758
- pubKeysByUri.set(uri, [key]);
1759
- }
1760
- for (const [docKey, doc] of documents) {
1761
- const site = doc?.record.site;
1762
- if (!site || !site.startsWith('at://'))
1763
- continue;
1764
- for (const pubKey of pubKeysByUri.get(site) ?? []) {
1765
- pairings.push({ docKey, pubKey });
1766
- }
1767
- }
1768
- }
1769
- // Per-record takedowns: null any entry whose subject URI is taken down.
1770
- for (const key of documents.keys()) {
1771
- const { uri } = parseGenericRecordKey(key);
1772
- if (labels.get(uri)?.isTakendown)
1773
- documents.set(key, null);
1774
- }
1775
- for (const key of publications.keys()) {
1776
- const { uri } = parseGenericRecordKey(key);
1777
- if (labels.get(uri)?.isTakendown)
1778
- publications.set(key, null);
1779
- }
1780
- // Pair takedowns: if exactly one side of a pair was nulled above, mirror.
1781
- for (const { docKey, pubKey } of pairings) {
1782
- const doc = documents.get(docKey);
1783
- const pub = publications.get(pubKey);
1784
- if (doc === null && pub !== null)
1785
- publications.set(pubKey, null);
1786
- if (pub === null && doc !== null)
1787
- documents.set(docKey, null);
1788
- }
1789
- };
1790
1683
  const uriToRef = (uri) => {
1791
1684
  return { uri };
1792
1685
  };