Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ import { make } from 'common/store/analytics/actions'
import { ensureLoggedIn } from 'common/utils/ensureLoggedIn'
import { waitForWrite } from 'utils/sagaHelpers'

import {
hasPendingPlaylistUpdates,
isPlaylistConfirmerDone
} from './utils/hasPendingPlaylistUpdates'
import { optimisticUpdateCollection } from './utils/optimisticUpdateCollection'
import { addTrackToCollectionLineupIfViewing } from './utils/updateCollectionPageLineup'

const { setOptimisticChallengeCompleted } = audioRewardsPageActions

Expand Down Expand Up @@ -75,7 +80,9 @@ function* addTrackToPlaylistAsync(action: AddTrackToPlaylistAction) {
const isNative = yield* getContext('isNativeMobile')
const { generatePlaylistArtwork } = yield* getContext('imageUtils')

const playlist = yield* queryCollection(playlistId, { staleTime: 0 })
const pending = yield* hasPendingPlaylistUpdates(playlistId)
const queryOpts = pending ? {} : { staleTime: 0 }
const playlist = yield* queryCollection(playlistId, queryOpts)
const playlistTracks = yield* call(
queryTracks,
playlist?.playlist_contents.track_ids.map(({ track }) => track) ?? []
Expand Down Expand Up @@ -128,6 +135,12 @@ function* addTrackToPlaylistAsync(action: AddTrackToPlaylistAction) {

// Optimistic update #2 to show updated artwork
yield* call(optimisticUpdateCollection, updatedPlaylist)
yield* call(
addTrackToCollectionLineupIfViewing,
action.playlistId,
track,
trackUid
)

yield* call(
confirmAddTrackToPlaylist,
Expand Down Expand Up @@ -194,15 +207,12 @@ function* confirmAddTrackToPlaylist(

return playlistId
},
function* (confirmedPlaylistId: ID) {
const confirmedPlaylist = yield* call(
queryCollection,
confirmedPlaylistId
)

if (!confirmedPlaylist) return

yield* call(updateCollectionData, [confirmedPlaylist])
function* (_confirmedPlaylistId: ID) {
const done = yield* isPlaylistConfirmerDone(playlistId)
if (!done) return
// Don't refetch - the backend may not have propagated yet, and a refetch
// would overwrite our optimistic cache with stale data.
yield* call(updateCollectionData, [playlist])
},
function* ({ error, timeout, message }) {
// Fail Call
Expand Down
61 changes: 40 additions & 21 deletions packages/web/src/common/store/cache/collections/commonSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ import { watchAddTrackToPlaylist } from './addTrackToPlaylistSaga'
import { confirmOrderPlaylist } from './confirmOrderPlaylist'
import { createAlbumSaga } from './createAlbumSaga'
import { createPlaylistSaga } from './createPlaylistSaga'
import {
hasPendingPlaylistUpdates,
isPlaylistConfirmerDone
} from './utils/hasPendingPlaylistUpdates'
import { optimisticUpdateCollection } from './utils/optimisticUpdateCollection'
import { updateCollectionLineupOrderIfViewing } from './utils/updateCollectionPageLineup'

const { manualClearToast, toast } = toastActions

Expand Down Expand Up @@ -101,9 +106,11 @@ function* editPlaylistAsync(
})
)

const pending = yield* hasPendingPlaylistUpdates(playlistId)
const queryOpts = pending ? {} : { staleTime: 0 }
let playlist: Collection = { ...formFields }
const playlistTracks = yield* call(queryCollectionTracks, playlistId, {
staleTime: 0
...queryOpts
})
const updatedTracks = (yield* all(
formFields.playlist_contents.track_ids.map(({ track }) =>
Expand Down Expand Up @@ -144,17 +151,20 @@ function* editPlaylistAsync(
yield* put(toast({ content: messages.editToast }))

if (playlistBeforeEdit?.is_private && !playlist.is_private) {
const playlistTracks = yield* call(queryCollectionTracks, playlistId)
const playlistTracksForPublish = yield* call(
queryCollectionTracks,
playlistId
)

// Publish all hidden tracks
// If the playlist is a scheduled release
// AND all tracks are scheduled releases, publish them all
const isEachTrackScheduled = playlistTracks?.every(
const isEachTrackScheduled = playlistTracksForPublish?.every(
(track) => track.is_unlisted && track.is_scheduled_release
)
const isEarlyRelease =
playlistBeforeEdit.is_scheduled_release && isEachTrackScheduled
for (const track of playlistTracks ?? []) {
for (const track of playlistTracksForPublish ?? []) {
if (
track.is_unlisted &&
(!track.is_scheduled_release || isEarlyRelease)
Expand Down Expand Up @@ -209,6 +219,8 @@ function* confirmEditPlaylist(
return playlist?.[0] ? userCollectionMetadataFromSDK(playlist[0]) : null
},
function* (confirmedPlaylist: Collection) {
const done = yield* isPlaylistConfirmerDone(playlistId)
if (!done) return
yield* call(updateCollectionData, [confirmedPlaylist])
},
function* ({ error, timeout, message }) {
Expand Down Expand Up @@ -243,9 +255,11 @@ function* removeTrackFromPlaylistAsync(
const userId = yield* call(ensureLoggedIn)
const { generatePlaylistArtwork } = yield* getContext('imageUtils')

const playlist = yield* queryCollection(playlistId, { staleTime: 0 })
const pending = yield* hasPendingPlaylistUpdates(playlistId)
const queryOpts = pending ? {} : { staleTime: 0 }
const playlist = yield* queryCollection(playlistId, queryOpts)
const playlistTracks = yield* call(queryCollectionTracks, playlistId, {
staleTime: 0
...queryOpts
})
const removedTrack = yield* queryTrack(trackId)

Expand Down Expand Up @@ -279,6 +293,11 @@ function* removeTrackFromPlaylistAsync(
})
)

yield* call(optimisticUpdateCollection, {
...updatedPlaylist,
track_count: count
})
// UI already dispatches lineup remove - skip full refresh to preserve scroll
yield* call(
confirmRemoveTrackFromPlaylist,
userId,
Expand All @@ -288,10 +307,6 @@ function* removeTrackFromPlaylistAsync(
count,
updatedPlaylist
)
yield* call(optimisticUpdateCollection, {
...updatedPlaylist,
track_count: count
})
}

function* confirmRemoveTrackFromPlaylist(
Expand Down Expand Up @@ -321,13 +336,13 @@ function* confirmRemoveTrackFromPlaylist(
})
return confirmedPlaylistId
},
function* (confirmedPlaylistId: ID) {
const confirmedPlaylist = yield* call(
queryCollection,
confirmedPlaylistId
)
if (!confirmedPlaylist) return
yield* call(updateCollectionData, [confirmedPlaylist])
function* (_confirmedPlaylistId: ID) {
const done = yield* isPlaylistConfirmerDone(playlistId)
if (!done) return
// Don't refetch - the backend may not have propagated yet, and a refetch
// would overwrite our optimistic cache with stale data (tracks reappearing).
// Our playlist state is correct since we just sent it to the API.
yield* call(updateCollectionData, [playlist])
yield* put(manualClearToast({ key: `remove-track-${trackId}` }))
yield* put(
toast({
Expand Down Expand Up @@ -372,10 +387,12 @@ function* orderPlaylistAsync(
const userId = yield* call(ensureLoggedIn)
const { generatePlaylistArtwork } = yield* getContext('imageUtils')

const pending = yield* hasPendingPlaylistUpdates(playlistId)
const queryOpts = pending ? {} : { staleTime: 0 }
const oldPlaylist = yield* queryCollection(playlistId)
const freshPlaylist = yield* queryCollection(playlistId, { staleTime: 0 })
const freshPlaylist = yield* queryCollection(playlistId, queryOpts)
const tracks = yield* call(queryCollectionTracks, playlistId, {
staleTime: 0
...queryOpts
})

const oldTracks =
Expand Down Expand Up @@ -427,15 +444,15 @@ function* orderPlaylistAsync(
({ id, time }) => ({ track: id, time })
)

yield* call(optimisticUpdateCollection, updatedPlaylist)
yield* call(updateCollectionLineupOrderIfViewing, action.playlistId, trackIds)
yield* call(
confirmOrderPlaylist,
userId,
playlistId,
trackIds,
updatedPlaylist
)

yield* call(optimisticUpdateCollection, updatedPlaylist)
}

/** PUBLISH PLAYLIST */
Expand Down Expand Up @@ -506,6 +523,8 @@ function* confirmPublishPlaylist(
return data?.[0] ? userCollectionMetadataFromSDK(data[0]) : null
},
function* (confirmedPlaylist: Collection) {
const done = yield* isPlaylistConfirmerDone(playlistId)
if (!done) return
confirmedPlaylist.is_private = false
confirmedPlaylist._is_publishing = false
yield* call(updateCollectionData, [confirmedPlaylist])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { playlistMetadataForUpdateWithSDK } from '@audius/common/adapters'
import { queryCollection, updateCollectionData } from '@audius/common/api'
import { updateCollectionData } from '@audius/common/api'
import { Kind, Collection, ID } from '@audius/common/models'
import {
cacheCollectionsActions as collectionActions,
Expand All @@ -11,6 +11,8 @@ import { makeKindId } from '@audius/common/utils'
import { Id } from '@audius/sdk'
import { call, put } from 'typed-redux-saga'

import { isPlaylistConfirmerDone } from './utils/hasPendingPlaylistUpdates'

export function* confirmOrderPlaylist(
userId: ID,
playlistId: ID,
Expand All @@ -30,15 +32,12 @@ export function* confirmOrderPlaylist(

return playlistId
},
function* (confirmedPlaylistId: ID) {
const confirmedPlaylist = yield* call(
queryCollection,
confirmedPlaylistId
)

if (!confirmedPlaylist) return

yield* call(updateCollectionData, [confirmedPlaylist])
function* (_confirmedPlaylistId: ID) {
const done = yield* isPlaylistConfirmerDone(playlistId)
if (!done) return
// Don't refetch - the backend may not have propagated yet, and a refetch
// would overwrite our optimistic cache with stale data.
yield* call(updateCollectionData, [playlist])
},
function* ({ error, timeout }) {
// Fail Call
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Kind, ID } from '@audius/common/models'
import { confirmerSelectors } from '@audius/common/store'
import { makeKindId } from '@audius/common/utils'
import { select } from 'typed-redux-saga'

const { getIsDone } = confirmerSelectors

/** UID used by the confirmer for playlist updates. */
export const getCollectionConfirmerUid = (playlistId: ID) =>
makeKindId(Kind.COLLECTIONS, playlistId)

/**
* Returns true if there are incomplete confirmer calls for this playlist.
* When true, we should use cached data instead of refetching, since the cache
* has been optimistically updated by preceding operations. Refetching could
* return stale data before the write has propagated to the API.
* When false (all calls have completed), we should refetch to get latest from
* other tabs/sessions before making our update.
*/
export function* hasPendingPlaylistUpdates(playlistId: ID) {
const uid = getCollectionConfirmerUid(playlistId)
const done: boolean = yield* select(getIsDone, { uid })
return !done
}

/**
* Returns true when all confirmer calls for this playlist have completed.
* Use before overwriting cache in success callbacks - only update when done
* so we don't overwrite another operation's optimistic state.
*/
export function* isPlaylistConfirmerDone(playlistId: ID) {
const uid = getCollectionConfirmerUid(playlistId)
return yield* select(getIsDone, { uid })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ID } from '@audius/common/models'
import {
collectionPageLineupActions,
collectionPageSelectors
} from '@audius/common/store'
import { put, select } from 'typed-redux-saga'

/**
* Refreshes the collection page Redux lineup when the user is viewing the
* given playlist. The lineup saga will call getCollectionTracks, which reads
* from the React Query cache. After an optimistic update, this ensures the
* lineup (which the collection page displays) reflects the updated collection.
*/
export function* refreshCollectionPageLineupIfViewing(playlistId: ID) {
const collectionId: ID | null = yield* select(
collectionPageSelectors.getCollectionId
)
if (collectionId === playlistId) {
yield* put(
collectionPageLineupActions.fetchLineupMetadatas(
0,
200,
false,
undefined,
{ skipLoadingState: true }
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Kind } from '@audius/common/models'
import type { Track, ID, UID } from '@audius/common/models'
import {
collectionPageLineupActions,
collectionPageSelectors
} from '@audius/common/store'
import { dayjs } from '@audius/common/utils'
import { put, select } from 'typed-redux-saga'

/**
* Updates the collection page lineup with minimal changes (add/remove/order)
* instead of a full refresh. This preserves scroll position since we're not
* replacing the entire entries array.
*/
export function* addTrackToCollectionLineupIfViewing(
playlistId: ID,
track: Track,
trackUid: UID
) {
const collectionId: ID | null = yield* select(
collectionPageSelectors.getCollectionId
)
if (collectionId !== playlistId) return

const entry = {
...track,
kind: Kind.TRACKS,
id: track.track_id,
uid: trackUid,
dateAdded: dayjs()
}
yield* put(collectionPageLineupActions.add(entry, track.track_id))
}

export function* removeTrackFromCollectionLineupIfViewing(
playlistId: ID,
trackUid: UID
) {
const collectionId: ID | null = yield* select(
collectionPageSelectors.getCollectionId
)
if (collectionId !== playlistId) return

yield* put(collectionPageLineupActions.remove(Kind.TRACKS, trackUid))
}

export function* updateCollectionLineupOrderIfViewing(
playlistId: ID,
trackIdsInOrder: ID[]
) {
const collectionId: ID | null = yield* select(
collectionPageSelectors.getCollectionId
)
if (collectionId !== playlistId) return

const lineup = yield* select(
collectionPageSelectors.getCollectionTracksLineup
)
if (!lineup?.entries?.length) return

const idToUid = new Map<ID, UID>(
lineup.entries.map((e) => [
'track_id' in e ? e.track_id : (e as { playlist_id: ID }).playlist_id,
e.uid
])
)
const orderedUids = trackIdsInOrder
.map((id) => idToUid.get(id))
.filter((uid): uid is UID => !!uid)
if (orderedUids.length === trackIdsInOrder.length) {
yield* put(collectionPageLineupActions.updateLineupOrder(orderedUids))
}
}
Loading