about summary refs log tree commit diff
path: root/src/lib/api/index.ts
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-10-02 00:03:27 +0900
committerGitHub <noreply@github.com>2024-10-02 00:03:27 +0900
commita7ee561e4074f839f340c77a1f21c8f5657865c7 (patch)
treeb39bc90825b38f91b95a6f2b9dfb1a1307fd293f /src/lib/api/index.ts
parentb59d6dfbb5957d53e730b2a5096492e127e2818b (diff)
downloadvoidsky-a7ee561e4074f839f340c77a1f21c8f5657865c7.tar.zst
Paralellize image uploads (#5535)
* Remove unsafe type coercions from posting embed

* Extract resolveEmbed into a separate function

* Refactor to if-else because these are mutually exclusive

* Refactor resolveEmbed to early returns

* Separate resolving embed and media

* Parallelize image upload

* Prioritize not dropping media
Diffstat (limited to 'src/lib/api/index.ts')
-rw-r--r--src/lib/api/index.ts254
1 files changed, 117 insertions, 137 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 1e51c7f25..08d4cb962 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -13,6 +13,8 @@ import {
   RichText,
 } from '@atproto/api'
 
+import {isNetworkError} from '#/lib/strings/errors'
+import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
 import {logger} from '#/logger'
 import {ComposerImage, compressImage} from '#/state/gallery'
 import {writePostgateRecord} from '#/state/queries/postgate'
@@ -22,8 +24,6 @@ import {
   threadgateAllowUISettingToAllowRecordValue,
   writeThreadgateRecord,
 } from '#/state/queries/threadgate'
-import {isNetworkError} from 'lib/strings/errors'
-import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
 import {LinkMeta} from '../link-meta/link-meta'
 import {uploadBlob} from './upload-blob'
 
@@ -60,13 +60,6 @@ interface PostOpts {
 }
 
 export async function post(agent: BskyAgent, opts: PostOpts) {
-  let embed:
-    | AppBskyEmbedImages.Main
-    | AppBskyEmbedExternal.Main
-    | AppBskyEmbedRecord.Main
-    | AppBskyEmbedVideo.Main
-    | AppBskyEmbedRecordWithMedia.Main
-    | undefined
   let reply
   let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true})
 
@@ -77,134 +70,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
   rt = shortenLinks(rt)
   rt = stripInvalidMentions(rt)
 
-  // add quote embed if present
-  if (opts.quote) {
-    embed = {
-      $type: 'app.bsky.embed.record',
-      record: {
-        uri: opts.quote.uri,
-        cid: opts.quote.cid,
-      },
-    } as AppBskyEmbedRecord.Main
-  }
-
-  // add image embed if present
-  if (opts.images?.length) {
-    logger.debug(`Uploading images`, {
-      count: opts.images.length,
-    })
-
-    const images: AppBskyEmbedImages.Image[] = []
-    for (const image of opts.images) {
-      opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
-
-      logger.debug(`Compressing image`)
-      const {path, width, height, mime} = await compressImage(image)
-
-      logger.debug(`Uploading image`)
-      const res = await uploadBlob(agent, path, mime)
-
-      images.push({
-        image: res.data.blob,
-        alt: image.alt,
-        aspectRatio: {width, height},
-      })
-    }
-
-    if (opts.quote) {
-      embed = {
-        $type: 'app.bsky.embed.recordWithMedia',
-        record: embed,
-        media: {
-          $type: 'app.bsky.embed.images',
-          images,
-        },
-      } as AppBskyEmbedRecordWithMedia.Main
-    } else {
-      embed = {
-        $type: 'app.bsky.embed.images',
-        images,
-      } as AppBskyEmbedImages.Main
-    }
-  }
-
-  // add video embed if present
-  if (opts.video) {
-    const captions = await Promise.all(
-      opts.video.captions
-        .filter(caption => caption.lang !== '')
-        .map(async caption => {
-          const {data} = await agent.uploadBlob(caption.file, {
-            encoding: 'text/vtt',
-          })
-          return {lang: caption.lang, file: data.blob}
-        }),
-    )
-    if (opts.quote) {
-      embed = {
-        $type: 'app.bsky.embed.recordWithMedia',
-        record: embed,
-        media: {
-          $type: 'app.bsky.embed.video',
-          video: opts.video.blobRef,
-          alt: opts.video.altText || undefined,
-          captions: captions.length === 0 ? undefined : captions,
-          aspectRatio: opts.video.aspectRatio,
-        } as AppBskyEmbedVideo.Main,
-      } as AppBskyEmbedRecordWithMedia.Main
-    } else {
-      embed = {
-        $type: 'app.bsky.embed.video',
-        video: opts.video.blobRef,
-        alt: opts.video.altText || undefined,
-        captions: captions.length === 0 ? undefined : captions,
-        aspectRatio: opts.video.aspectRatio,
-      } as AppBskyEmbedVideo.Main
-    }
-  }
-
-  // add external embed if present
-  if (opts.extLink && !opts.images?.length) {
-    if (opts.extLink.embed) {
-      embed = opts.extLink.embed
-    } else {
-      let thumb
-      if (opts.extLink.localThumb) {
-        opts.onStateChange?.('Uploading link thumbnail...')
-
-        const {path, mime} = opts.extLink.localThumb.source
-        const res = await uploadBlob(agent, path, mime)
-
-        thumb = res.data.blob
-      }
-
-      if (opts.quote) {
-        embed = {
-          $type: 'app.bsky.embed.recordWithMedia',
-          record: embed,
-          media: {
-            $type: 'app.bsky.embed.external',
-            external: {
-              uri: opts.extLink.uri,
-              title: opts.extLink.meta?.title || '',
-              description: opts.extLink.meta?.description || '',
-              thumb,
-            },
-          } as AppBskyEmbedExternal.Main,
-        } as AppBskyEmbedRecordWithMedia.Main
-      } else {
-        embed = {
-          $type: 'app.bsky.embed.external',
-          external: {
-            uri: opts.extLink.uri,
-            title: opts.extLink.meta?.title || '',
-            description: opts.extLink.meta?.description || '',
-            thumb,
-          },
-        } as AppBskyEmbedExternal.Main
-      }
-    }
-  }
+  const embed = await resolveEmbed(agent, opts)
 
   // add replyTo if post is a reply to another post
   if (opts.replyTo) {
@@ -313,3 +179,117 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
 
   return res
 }
+
+async function resolveEmbed(
+  agent: BskyAgent,
+  opts: PostOpts,
+): Promise<
+  | AppBskyEmbedImages.Main
+  | AppBskyEmbedVideo.Main
+  | AppBskyEmbedExternal.Main
+  | AppBskyEmbedRecord.Main
+  | AppBskyEmbedRecordWithMedia.Main
+  | undefined
+> {
+  const media = await resolveMedia(agent, opts)
+  if (opts.quote) {
+    const quoteRecord = {
+      $type: 'app.bsky.embed.record',
+      record: {
+        uri: opts.quote.uri,
+        cid: opts.quote.cid,
+      },
+    }
+    if (media) {
+      return {
+        $type: 'app.bsky.embed.recordWithMedia',
+        record: quoteRecord,
+        media,
+      }
+    } else {
+      return quoteRecord
+    }
+  }
+  if (media) {
+    return media
+  }
+  if (opts.extLink?.embed) {
+    return opts.extLink.embed
+  }
+  return undefined
+}
+
+async function resolveMedia(
+  agent: BskyAgent,
+  opts: PostOpts,
+): Promise<
+  | AppBskyEmbedExternal.Main
+  | AppBskyEmbedImages.Main
+  | AppBskyEmbedVideo.Main
+  | undefined
+> {
+  if (opts.images?.length) {
+    logger.debug(`Uploading images`, {
+      count: opts.images.length,
+    })
+    opts.onStateChange?.(`Uploading images...`)
+    const images: AppBskyEmbedImages.Image[] = await Promise.all(
+      opts.images.map(async (image, i) => {
+        logger.debug(`Compressing image #${i}`)
+        const {path, width, height, mime} = await compressImage(image)
+        logger.debug(`Uploading image #${i}`)
+        const res = await uploadBlob(agent, path, mime)
+        return {
+          image: res.data.blob,
+          alt: image.alt,
+          aspectRatio: {width, height},
+        }
+      }),
+    )
+    return {
+      $type: 'app.bsky.embed.images',
+      images,
+    }
+  }
+  if (opts.video) {
+    const captions = await Promise.all(
+      opts.video.captions
+        .filter(caption => caption.lang !== '')
+        .map(async caption => {
+          const {data} = await agent.uploadBlob(caption.file, {
+            encoding: 'text/vtt',
+          })
+          return {lang: caption.lang, file: data.blob}
+        }),
+    )
+    return {
+      $type: 'app.bsky.embed.video',
+      video: opts.video.blobRef,
+      alt: opts.video.altText || undefined,
+      captions: captions.length === 0 ? undefined : captions,
+      aspectRatio: opts.video.aspectRatio,
+    }
+  }
+  if (opts.extLink) {
+    if (opts.extLink.embed) {
+      return undefined
+    }
+    let thumb
+    if (opts.extLink.localThumb) {
+      opts.onStateChange?.('Uploading link thumbnail...')
+      const {path, mime} = opts.extLink.localThumb.source
+      const res = await uploadBlob(agent, path, mime)
+      thumb = res.data.blob
+    }
+    return {
+      $type: 'app.bsky.embed.external',
+      external: {
+        uri: opts.extLink.uri,
+        title: opts.extLink.meta?.title || '',
+        description: opts.extLink.meta?.description || '',
+        thumb,
+      },
+    }
+  }
+  return undefined
+}