From e2c89b66e94473607eb5845e11f0b7304ae95bd5 Mon Sep 17 00:00:00 2001 From: Max Carlson <> Date: Fri, 13 Mar 2026 12:17:33 +0800 Subject: [PATCH 1/2] fix: deduplicate concurrent WorkOS session refreshes When multiple requests arrive with the same expired session cookie, only one refresh call is made to WorkOS. Concurrent requests await the same promise instead of each consuming the single-use refresh token independently. Fixes studio page sign-out caused by batch submission race condition. Co-Authored-By: Claude Opus 4.6 --- src/utils/workos.util.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils/workos.util.ts b/src/utils/workos.util.ts index 9480a048..5b5d1739 100644 --- a/src/utils/workos.util.ts +++ b/src/utils/workos.util.ts @@ -49,6 +49,13 @@ export const authenticateAndGetWorkOSSession = async (authToken: string) => { }); }; +// Deduplicates concurrent refresh attempts for the same expired session token. +// WorkOS refresh tokens are single-use, so only one refresh per token should be made. +const pendingRefreshes = new Map< + string, + Promise<{ authenticated: boolean; sealedSession?: string }> +>(); + // Common authentication logic export const authenticateWorkOS = async ( authToken: string | undefined, @@ -64,7 +71,14 @@ export const authenticateWorkOS = async ( let session = await authenticateAndGetWorkOSSession(authToken); if (!session) { - const refreshResult = await refreshWorkOSSession(authToken); + let refreshPromise = pendingRefreshes.get(authToken); + if (!refreshPromise) { + refreshPromise = refreshWorkOSSession(authToken).finally(() => { + pendingRefreshes.delete(authToken); + }); + pendingRefreshes.set(authToken, refreshPromise); + } + const refreshResult = await refreshPromise; if (!refreshResult.authenticated || !refreshResult.sealedSession) { return null; } From ea0c2f1fad470c1dfba3838f8f8cc8862c21c083 Mon Sep 17 00:00:00 2001 From: vuledev <77647824+mystixxx@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:02:18 +0200 Subject: [PATCH 2/2] fix: bust CDN cache on re-render by versioning asset paths --- src/controllers/dream.controller.ts | 44 +++++++++++++++++++++++--- src/controllers/keyframe.controller.ts | 9 ++++++ src/controllers/playlist.controller.ts | 24 +++++++------- src/controllers/user.controller.ts | 20 ++++++------ src/utils/r2.util.ts | 19 +++++++++-- src/utils/uploadVersion.util.ts | 38 ++++++++++++++++++++++ 6 files changed, 124 insertions(+), 30 deletions(-) create mode 100644 src/utils/uploadVersion.util.ts diff --git a/src/controllers/dream.controller.ts b/src/controllers/dream.controller.ts index 00cfe1c2..f3cae5f5 100644 --- a/src/controllers/dream.controller.ts +++ b/src/controllers/dream.controller.ts @@ -85,6 +85,14 @@ import { transformDreamsWithSignedUrls, } from "utils/transform.util"; import { detectMediaTypeFromExtension } from "utils/media.util"; +import { + clearFilmstripVersion, + delThumbVersion, + getFilmstripVersion, + getThumbVersion, + setFilmstripVersion, + setThumbVersion, +} from "utils/uploadVersion.util"; /** * Handles get dreams @@ -372,17 +380,21 @@ export const handleCreateMultipartUploadDreamFile = async ( * filePath r2 generation */ if (type === DreamFileType.THUMBNAIL) { + const renderVersion = await setThumbVersion(dreamUUID); filePath = generateThumbnailPath({ userIdentifier, dreamUUID, extension: fileExtension, + renderVersion, }); } else if (type === DreamFileType.FILMSTRIP) { + const renderVersion = await setFilmstripVersion(dreamUUID); filePath = generateFilmstripPath({ userIdentifier, dreamUUID, extension: fileExtension, frameNumber: frameNumber!, + renderVersion, }); } else if (type === DreamFileType.DREAM) { filePath = generateDreamPath({ @@ -478,6 +490,7 @@ export const handleRefreshMultipartUploadUrl = async ( userIdentifier, dreamUUID, extension: fileExtension, + renderVersion: await getThumbVersion(dreamUUID), }); } else if (type === DreamFileType.FILMSTRIP) { filePath = generateFilmstripPath({ @@ -485,6 +498,7 @@ export const handleRefreshMultipartUploadUrl = async ( dreamUUID, extension: fileExtension, frameNumber: frameNumber!, + renderVersion: await getFilmstripVersion(dreamUUID), }); } else if (type === DreamFileType.DREAM) { filePath = generateDreamPath({ @@ -579,6 +593,7 @@ export const handleCompleteMultipartUpload = async ( userIdentifier, dreamUUID, extension: fileExtension, + renderVersion: await getThumbVersion(dreamUUID), }); /** @@ -587,12 +602,14 @@ export const handleCompleteMultipartUpload = async ( await dreamRepository.update(dream.id, { thumbnail: filePath, }); + await delThumbVersion(dreamUUID); } else if (type === DreamFileType.FILMSTRIP) { filePath = generateFilmstripPath({ userIdentifier, dreamUUID, extension: fileExtension, frameNumber: frameNumber!, + renderVersion: await getFilmstripVersion(dreamUUID), }); } else if (type === DreamFileType.DREAM && !processed) { filePath = generateDreamPath({ @@ -1030,6 +1047,8 @@ export const handleSetDreamStatusProcessing = async ( return handleNotFound(req as RequestType, res); } + await clearFilmstripVersion(dreamUUID); + const updatedDream = await dreamRepository.save({ ...dream, status: DreamStatusType.PROCESSING, @@ -1091,15 +1110,25 @@ export const handleSetDreamStatusProcessed = async ( */ const user = dream.user; + const filmstripVersion = filmstrip + ? await getFilmstripVersion(dreamUUID) + : undefined; const formatedFilmstrip: Frame[] | undefined = filmstrip?.map( (frame) => ({ frameNumber: Number(frame), - url: `${getUserIdentifier( - user, - )}/${dreamUUID}/filmstrip/frame-${frame}.${FILE_EXTENSIONS.JPG}`, + url: filmstripVersion + ? `${getUserIdentifier( + user, + )}/${dreamUUID}/filmstrip/${filmstripVersion}/frame-${frame}.${ + FILE_EXTENSIONS.JPG + }` + : `${getUserIdentifier( + user, + )}/${dreamUUID}/filmstrip/frame-${frame}.${FILE_EXTENSIONS.JPG}`, }) as Frame, ); + if (filmstripVersion) await clearFilmstripVersion(dreamUUID); const updateData: Partial = { status: DreamStatusType.PROCESSED, @@ -1486,8 +1515,13 @@ export const handleUpdateThumbnailDream = async ( const bucketName = env.R2_BUCKET_NAME; const fileMymeType = req.file?.mimetype; const fileExtension = MYME_TYPES_EXTENSIONS[fileMymeType ?? MYME_TYPES.MP4]; - const fileName = `${dreamUUID}.${fileExtension}`; - const filePath = `${getUserIdentifier(user)}/${dreamUUID}/${fileName}`; + const userIdentifier = getUserIdentifier(dream.user); + const filePath = generateThumbnailPath({ + userIdentifier, + dreamUUID, + extension: fileExtension, + renderVersion: Date.now(), + }); if (thumbnailBuffer) { const command = new PutObjectCommand({ diff --git a/src/controllers/keyframe.controller.ts b/src/controllers/keyframe.controller.ts index b2e40312..e75fe1aa 100644 --- a/src/controllers/keyframe.controller.ts +++ b/src/controllers/keyframe.controller.ts @@ -30,6 +30,11 @@ import { generateKeyframePath, getUploadPartSignedUrl, } from "utils/r2.util"; +import { + delKeyframeVersion, + getKeyframeVersion, + setKeyframeVersion, +} from "utils/uploadVersion.util"; import { CreateMultipartUploadFileRequest } from "types/keyframe.types"; import { keyframeRepository, userRepository } from "database/repositories"; import { @@ -219,10 +224,12 @@ export const handleInitKeyframeImageUpload = async ( /** * filePath r2 generation */ + const renderVersion = await setKeyframeVersion(keyframeUUID); const filePath = generateKeyframePath({ userIdentifier, keyframeUUID, extension: fileExtension, + renderVersion, }); const uploadId = await createMultipartUpload(filePath!); @@ -304,11 +311,13 @@ export const handleCompleteKeyframeImageUpload = async ( userIdentifier, keyframeUUID, extension: fileExtension, + renderVersion: await getKeyframeVersion(keyframeUUID), }); await keyframeRepository.update(keyframe.id, { image: filePath!, }); + await delKeyframeVersion(keyframeUUID); /** * completes multipart upload with path, upload id and parts diff --git a/src/controllers/playlist.controller.ts b/src/controllers/playlist.controller.ts index 6dec86f2..faa309d3 100644 --- a/src/controllers/playlist.controller.ts +++ b/src/controllers/playlist.controller.ts @@ -453,15 +453,15 @@ export const handleGetPlaylistReferences = async ( const unfilteredReferences = await playlistItemRepository.find({ where: Array.isArray(where) ? where.map((playlistWhere) => ({ - playlistItem: { uuid }, - type: PlaylistItemType.PLAYLIST, - playlist: playlistWhere, - })) + playlistItem: { uuid }, + type: PlaylistItemType.PLAYLIST, + playlist: playlistWhere, + })) : { - playlistItem: { uuid }, - type: PlaylistItemType.PLAYLIST, - playlist: where, - }, + playlistItem: { uuid }, + type: PlaylistItemType.PLAYLIST, + playlist: where, + }, select: { id: true, type: true, @@ -557,9 +557,9 @@ export const handleGetPlaylists = async ( const userCondition = { uuid: userUUID }; const where = searchILike ? [ - { user: userCondition, name: searchILike }, - { user: userCondition, displayedOwner: { name: searchILike } }, - ] + { user: userCondition, name: searchILike }, + { user: userCondition, displayedOwner: { name: searchILike } }, + ] : { user: userCondition }; try { @@ -847,7 +847,7 @@ export const handleUpdateThumbnailPlaylist = async ( const fileMymeType = req.file?.mimetype; const fileExtension = MYME_TYPES_EXTENSIONS[fileMymeType ?? MYME_TYPES.JPEG]; - const fileName = `${THUMBNAIL}.${fileExtension}`; + const fileName = `${THUMBNAIL}-${Date.now()}.${fileExtension}`; const filePath = `${getUserIdentifier(user)}/${PLAYLIST_PREFIX}-${ playlist.id }/${fileName}`; diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 72d37cbc..1bdc650d 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -142,15 +142,15 @@ export const handleGetUsers = async ( }; const whereSentence = search ? [ - { - ...baseConditions, - name: ILike(`%${search}%`), - }, - { - ...baseConditions, - email: ILike(`%${search}%`), - }, - ] + { + ...baseConditions, + name: ILike(`%${search}%`), + }, + { + ...baseConditions, + email: ILike(`%${search}%`), + }, + ] : (baseConditions as FindOptionsWhere); const [users, count] = await userRepository.findAndCount({ where: whereSentence, @@ -543,7 +543,7 @@ export const handleUpdateUserAvatar = async ( const fileMymeType = req.file?.mimetype; const fileExtension = MYME_TYPES_EXTENSIONS[fileMymeType ?? MYME_TYPES.JPEG]; - const fileName = `${AVATAR}.${fileExtension}`; + const fileName = `${AVATAR}-${Date.now()}.${fileExtension}`; const filePath = `${getUserIdentifier(user)}/${fileName}`; if (avatarBuffer) { diff --git a/src/utils/r2.util.ts b/src/utils/r2.util.ts index 005d6df8..52322055 100644 --- a/src/utils/r2.util.ts +++ b/src/utils/r2.util.ts @@ -140,24 +140,33 @@ export const generateThumbnailPath = ({ userIdentifier, dreamUUID, extension, + renderVersion, }: { userIdentifier: string; dreamUUID: string; extension: string; -}) => `${userIdentifier}/${dreamUUID}/thumbnails/${dreamUUID}.${extension}`; + renderVersion?: number; +}) => + renderVersion + ? `${userIdentifier}/${dreamUUID}/thumbnails/${dreamUUID}-${renderVersion}.${extension}` + : `${userIdentifier}/${dreamUUID}/thumbnails/${dreamUUID}.${extension}`; export const generateFilmstripPath = ({ userIdentifier, dreamUUID, extension, frameNumber, + renderVersion, }: { userIdentifier: string; dreamUUID: string; extension: string; frameNumber: number; + renderVersion?: number; }) => - `${userIdentifier}/${dreamUUID}/filmstrip/frame-${frameNumber}.${extension}`; + renderVersion + ? `${userIdentifier}/${dreamUUID}/filmstrip/${renderVersion}/frame-${frameNumber}.${extension}` + : `${userIdentifier}/${dreamUUID}/filmstrip/frame-${frameNumber}.${extension}`; export const generateDreamPath = ({ userIdentifier, @@ -178,12 +187,16 @@ export const generateKeyframePath = ({ userIdentifier, keyframeUUID, extension, + renderVersion, }: { userIdentifier: string; keyframeUUID: string; extension: string; + renderVersion?: number; }) => - `${userIdentifier}/keyframes/${keyframeUUID}/${keyframeUUID}.${extension}`; + renderVersion + ? `${userIdentifier}/keyframes/${keyframeUUID}/${keyframeUUID}-${renderVersion}.${extension}` + : `${userIdentifier}/keyframes/${keyframeUUID}/${keyframeUUID}.${extension}`; /** * Helper function to extract file extension from object key or file path diff --git a/src/utils/uploadVersion.util.ts b/src/utils/uploadVersion.util.ts new file mode 100644 index 00000000..735648ef --- /dev/null +++ b/src/utils/uploadVersion.util.ts @@ -0,0 +1,38 @@ +import { redisClient } from "clients/redis.client"; + +const TTL = 7200; // 2 hours — enough for any upload to complete + +const toNumber = (val: string | null): number | undefined => + val ? Number(val) : undefined; + +const makeVersionStore = (prefix: string, nx = false) => ({ + set: async (uuid: string): Promise => { + const key = `upload:version:${prefix}:${uuid}`; + const version = Date.now(); + if (nx) { + await redisClient.set(key, version, "EX", TTL, "NX"); + return toNumber(await redisClient.get(key)) ?? version; + } + await redisClient.set(key, version, "EX", TTL); + return version; + }, + get: (uuid: string) => + redisClient.get(`upload:version:${prefix}:${uuid}`).then(toNumber), + del: (uuid: string) => redisClient.del(`upload:version:${prefix}:${uuid}`), +}); + +const thumbStore = makeVersionStore("thumb"); +const filmstripStore = makeVersionStore("filmstrip", true); +const keyframeStore = makeVersionStore("keyframe"); + +export const setThumbVersion = thumbStore.set; +export const getThumbVersion = thumbStore.get; +export const delThumbVersion = thumbStore.del; + +export const setFilmstripVersion = filmstripStore.set; +export const getFilmstripVersion = filmstripStore.get; +export const clearFilmstripVersion = filmstripStore.del; + +export const setKeyframeVersion = keyframeStore.set; +export const getKeyframeVersion = keyframeStore.get; +export const delKeyframeVersion = keyframeStore.del;