From cdbba4c9474ef27af68aa93df2f8d5c2034ace8f Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 11:07:17 +0000 Subject: [PATCH 01/38] feat(releases): initial support for stubbed org level release limits --- .../upsell/ReleasesUpsellProvider.tsx | 205 +++++++++++++++++- 1 file changed, 197 insertions(+), 8 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 51569d3b04f..9d6aff6af0d 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,9 +1,21 @@ import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' +import {useObservable} from 'react-rx' +import {BehaviorSubject, firstValueFrom, of} from 'rxjs' +import { + delay, + distinctUntilChanged, + shareReplay, + startWith, + switchMap, + take, + tap, +} from 'rxjs/operators' import {ReleasesUpsellContext} from 'sanity/_singletons' import {useClient, useFeatureEnabled, useProjectId} from '../../../hooks' +import {useResourceCache} from '../../../store/_legacy/ResourceCacheProvider' import { UpsellDialogDismissed, UpsellDialogLearnMoreCtaClicked, @@ -16,6 +28,12 @@ import {UpsellDialog} from '../../../studio/upsell/UpsellDialog' import {useActiveReleases} from '../../store/useActiveReleases' import {type ReleasesUpsellContextValue} from './types' +type ReleaseLimits = { + datasetReleaseLimit: number + orgActiveReleaseCount: number + orgActiveReleaseLimit: number +} + class StudioReleaseLimitExceededError extends Error { details: {type: 'releaseLimitExceededError'} @@ -33,6 +51,19 @@ const BASE_URL = 'www.sanity.io' // Date when the change from array to object in the data returned was introduced. const API_VERSION = '2024-04-19' +const fetchReleasesLimits = () => + of({ + orgActiveReleaseCount: 10, + orgActiveReleaseLimit: 20, + datasetReleaseLimit: 6, + }).pipe( + tap(() => console.log('fetchReleasesLimits')), + delay(3000), + ) + +const cacheTrigger$ = new BehaviorSubject(null) +const CACHE_TTL_MS = 15000 // 1 minute + /** * @beta * @hidden @@ -156,19 +187,177 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { }) }, [telemetry]) + const activeReleasesCount = activeReleases?.length || 0 + + const resourceCache = useResourceCache() + + const cache$ = useMemo(() => { + return cacheTrigger$.pipe( + distinctUntilChanged(), + switchMap((_activeReleases) => { + if (_activeReleases === null) { + return of(null) + } + + const now = Date.now() + const _cachedState = resourceCache.get<{ + datasetLimit: number | null + cacheExpiresAt: number | null + cachedValue: any + activeReleases: number + expired?: boolean + }>({ + namespace: 'ReleasesUpsellLimits', + dependencies: [activeReleases], + }) + + if (_cachedState) { + const { + datasetLimit, + cacheExpiresAt, + cachedValue, + activeReleases: cachedActiveReleases, + expired, + } = _cachedState + + if (datasetLimit !== null && _activeReleases === datasetLimit && cachedValue) { + console.log('At dataset limit, keeping cache indefinitely.') + return of(cachedValue) + } + + if (expired) { + console.log('Cache is expired but will NOT fetch until guard is called.') + return of(null) + } + + if (cacheExpiresAt && now < cacheExpiresAt && cachedValue) { + return of(cachedValue) + } + + if (cachedActiveReleases === _activeReleases && cachedValue) { + return of(cachedValue) + } + } + + return of(null) + }), + startWith(null), + shareReplay({bufferSize: 1, refCount: true}), + ) + }, [activeReleases, resourceCache]) + + const releaseLimits = useObservable(cache$, null) + + useEffect(() => { + const _cachedState = resourceCache.get<{ + datasetLimit: number | null + cacheExpiresAt: number | null + cachedValue: any + activeReleases: number + }>({ + namespace: 'ReleasesUpsellLimits', + dependencies: [activeReleases], + }) + + if (!_cachedState) return + + const {cacheExpiresAt} = _cachedState + const now = Date.now() + + if (cacheExpiresAt !== null && now >= cacheExpiresAt) { + console.log('Cache TTL expired, marking cache as expired...') + resourceCache.set({ + namespace: 'ReleasesUpsellLimits', + dependencies: [activeReleases], + value: {..._cachedState, expired: true}, + }) + } + }, [activeReleases, resourceCache]) + const guardWithReleaseLimitUpsell = useCallback( - (cb: () => void, throwError: boolean = false) => { - if (mode === 'default') { - return cb() + async (cb: () => void, throwError: boolean = false) => { + let limits = releaseLimits + + const _cachedState = resourceCache.get<{ + datasetLimit: number | null + cacheExpiresAt: number | null + cachedValue: any + expired?: boolean + }>({ + namespace: 'ReleasesUpsellLimits', + dependencies: [activeReleases], + }) + + const now = Date.now() + + if ( + _cachedState && + _cachedState.cachedValue && + !_cachedState.expired && + now < _cachedState.cacheExpiresAt! + ) { + console.log('Using cached limits from ResourceCache') + limits = _cachedState.cachedValue + } + + if ( + _cachedState && + _cachedState.datasetLimit !== null && + activeReleases.length === _cachedState.datasetLimit + ) { + console.log('At dataset limit, NOT fetching new data.') + limits = _cachedState.cachedValue } - handleOpenDialog() - if (throwError) { - throw new StudioReleaseLimitExceededError() + if (!limits || _cachedState?.expired) { + console.log('Cache is expired or missing. Fetching new data...') + + try { + limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) + + console.log('Received first API response', limits) + + resourceCache.set({ + dependencies: [activeReleases], + namespace: 'ReleasesUpsellLimits', + value: { + datasetLimit: limits.datasetReleaseLimit, + cacheExpiresAt: Date.now() + CACHE_TTL_MS, + cachedValue: limits, + activeReleases: activeReleasesCount, + }, + }) + } catch (error) { + console.error('Error fetching release limits:', error) + return + } } - return false + + if (!limits) { + console.warn('Fetch was triggered, but still no limits found. Skipping callback execution.') + return + } + + if ('error' in limits) return cb() + + const {datasetReleaseLimit, orgActiveReleaseCount, orgActiveReleaseLimit} = limits + + const shouldShowDialog = + activeReleasesCount >= datasetReleaseLimit || + (orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit) + + if (shouldShowDialog) { + handleOpenDialog() + + if (throwError) { + throw new StudioReleaseLimitExceededError() + } + return + } + + cb() }, - [handleOpenDialog, mode], + [activeReleases, activeReleasesCount, handleOpenDialog, releaseLimits, resourceCache], ) const onReleaseLimitReached = useCallback( From 53e38c2c8a9a7f381a370d15a79b6320e8760853 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 11:39:06 +0000 Subject: [PATCH 02/38] refactor: removing datasetLimit from cache and just using cacheValue --- .../contexts/upsell/ReleasesUpsellProvider.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 9d6aff6af0d..6813e002708 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -201,7 +201,6 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const now = Date.now() const _cachedState = resourceCache.get<{ - datasetLimit: number | null cacheExpiresAt: number | null cachedValue: any activeReleases: number @@ -213,14 +212,17 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { if (_cachedState) { const { - datasetLimit, cacheExpiresAt, cachedValue, activeReleases: cachedActiveReleases, expired, } = _cachedState - if (datasetLimit !== null && _activeReleases === datasetLimit && cachedValue) { + if ( + cachedValue?.datasetReleaseLimit !== null && + _activeReleases === cachedValue?.datasetReleaseLimit && + cachedValue + ) { console.log('At dataset limit, keeping cache indefinitely.') return of(cachedValue) } @@ -250,7 +252,6 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { useEffect(() => { const _cachedState = resourceCache.get<{ - datasetLimit: number | null cacheExpiresAt: number | null cachedValue: any activeReleases: number @@ -279,7 +280,6 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { let limits = releaseLimits const _cachedState = resourceCache.get<{ - datasetLimit: number | null cacheExpiresAt: number | null cachedValue: any expired?: boolean @@ -302,8 +302,8 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { if ( _cachedState && - _cachedState.datasetLimit !== null && - activeReleases.length === _cachedState.datasetLimit + _cachedState.cachedValue.datasetReleaseLimit !== null && + activeReleases.length === _cachedState.cachedValue.datasetReleaseLimit ) { console.log('At dataset limit, NOT fetching new data.') limits = _cachedState.cachedValue @@ -321,7 +321,6 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { dependencies: [activeReleases], namespace: 'ReleasesUpsellLimits', value: { - datasetLimit: limits.datasetReleaseLimit, cacheExpiresAt: Date.now() + CACHE_TTL_MS, cachedValue: limits, activeReleases: activeReleasesCount, From d3c8a78399fb9c258d25faf7b882269a6b6a87b4 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 12:10:20 +0000 Subject: [PATCH 03/38] fix: caching when activeReleases is at the org level limit too --- .../upsell/ReleasesUpsellProvider.tsx | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 6813e002708..d0e3369ea59 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -56,6 +56,10 @@ const fetchReleasesLimits = () => orgActiveReleaseCount: 10, orgActiveReleaseLimit: 20, datasetReleaseLimit: 6, + + // orgActiveReleaseCount: 6, + // orgActiveReleaseLimit: 6, + // datasetReleaseLimit: 10, }).pipe( tap(() => console.log('fetchReleasesLimits')), delay(3000), @@ -218,15 +222,19 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { expired, } = _cachedState - if ( - cachedValue?.datasetReleaseLimit !== null && - _activeReleases === cachedValue?.datasetReleaseLimit && - cachedValue - ) { + const datasetLimit = cachedValue?.datasetReleaseLimit + const orgLimit = cachedValue?.orgActiveReleaseLimit + + if (datasetLimit !== null && _activeReleases === datasetLimit && cachedValue) { console.log('At dataset limit, keeping cache indefinitely.') return of(cachedValue) } + if (orgLimit !== null && _activeReleases === orgLimit && cachedValue) { + console.log('At org limit, keeping cache indefinitely.') + return of(cachedValue) + } + if (expired) { console.log('Cache is expired but will NOT fetch until guard is called.') return of(null) @@ -262,9 +270,20 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { if (!_cachedState) return - const {cacheExpiresAt} = _cachedState + const {cacheExpiresAt, cachedValue} = _cachedState const now = Date.now() + const datasetLimit = cachedValue?.datasetReleaseLimit + const orgLimit = cachedValue?.orgActiveReleaseLimit + + if ( + (datasetLimit !== null && activeReleases.length === datasetLimit) || + (orgLimit !== null && activeReleases.length === orgLimit) + ) { + console.log('At limit (dataset or org), keeping cache indefinitely.') + return + } + if (cacheExpiresAt !== null && now >= cacheExpiresAt) { console.log('Cache TTL expired, marking cache as expired...') resourceCache.set({ @@ -300,13 +319,15 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { limits = _cachedState.cachedValue } + const datasetLimit = _cachedState?.cachedValue?.datasetReleaseLimit + const orgLimit = _cachedState?.cachedValue?.orgActiveReleaseLimit + if ( - _cachedState && - _cachedState.cachedValue.datasetReleaseLimit !== null && - activeReleases.length === _cachedState.cachedValue.datasetReleaseLimit + (datasetLimit !== null && activeReleases.length === datasetLimit) || + (orgLimit !== null && activeReleases.length === orgLimit) ) { - console.log('At dataset limit, NOT fetching new data.') - limits = _cachedState.cachedValue + console.log('At limit (dataset or org), NOT fetching new data.') + limits = _cachedState?.cachedValue } if (!limits || _cachedState?.expired) { From 8d533735cbaf78314e7c0b9916b36539c70f40d1 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 12:59:22 +0000 Subject: [PATCH 04/38] feat(releases): initial support for stubbed org level release limits --- .../upsell/ReleasesUpsellProvider.tsx | 178 +++++------------- 1 file changed, 46 insertions(+), 132 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index d0e3369ea59..eb3bbd73354 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -2,10 +2,11 @@ import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' import {useObservable} from 'react-rx' -import {BehaviorSubject, firstValueFrom, of} from 'rxjs' +import {BehaviorSubject, firstValueFrom, merge, of, timer} from 'rxjs' import { delay, distinctUntilChanged, + map, shareReplay, startWith, switchMap, @@ -191,147 +192,59 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { }) }, [telemetry]) - const activeReleasesCount = activeReleases?.length || 0 - const resourceCache = useResourceCache() - const cache$ = useMemo(() => { - return cacheTrigger$.pipe( - distinctUntilChanged(), - switchMap((_activeReleases) => { - if (_activeReleases === null) { - return of(null) - } - - const now = Date.now() - const _cachedState = resourceCache.get<{ - cacheExpiresAt: number | null - cachedValue: any - activeReleases: number - expired?: boolean - }>({ - namespace: 'ReleasesUpsellLimits', - dependencies: [activeReleases], - }) - - if (_cachedState) { - const { - cacheExpiresAt, - cachedValue, - activeReleases: cachedActiveReleases, - expired, - } = _cachedState - - const datasetLimit = cachedValue?.datasetReleaseLimit - const orgLimit = cachedValue?.orgActiveReleaseLimit - - if (datasetLimit !== null && _activeReleases === datasetLimit && cachedValue) { - console.log('At dataset limit, keeping cache indefinitely.') - return of(cachedValue) - } - - if (orgLimit !== null && _activeReleases === orgLimit && cachedValue) { - console.log('At org limit, keeping cache indefinitely.') - return of(cachedValue) - } - - if (expired) { - console.log('Cache is expired but will NOT fetch until guard is called.') - return of(null) - } - - if (cacheExpiresAt && now < cacheExpiresAt && cachedValue) { - return of(cachedValue) - } + const cache$ = useMemo( + () => + cacheTrigger$.pipe( + distinctUntilChanged(), + switchMap((activeReleasesCount) => { + if (activeReleasesCount === null) return of(null) + + const cachedState = resourceCache.get<{ + cachedValue: any + activeReleases: number + }>({ + namespace: 'ReleasesUpsellLimits', + dependencies: [activeReleases], + }) - if (cachedActiveReleases === _activeReleases && cachedValue) { - return of(cachedValue) + if (cachedState) { + const {cachedValue, activeReleases: cachedReleases} = cachedState + + if (cachedReleases === activeReleasesCount) { + console.log('Using cached value.') + + return merge( + of(cachedValue), + timer(CACHE_TTL_MS).pipe( + map(() => { + console.log('TTL expired, emitting null.') + cacheTrigger$.next(null) + return null + }), + ), + ) + } } - } - return of(null) - }), - startWith(null), - shareReplay({bufferSize: 1, refCount: true}), - ) - }, [activeReleases, resourceCache]) + return of(null) + }), + startWith(null), + shareReplay({bufferSize: 1, refCount: true}), + ), + [activeReleases, resourceCache], + ) const releaseLimits = useObservable(cache$, null) - useEffect(() => { - const _cachedState = resourceCache.get<{ - cacheExpiresAt: number | null - cachedValue: any - activeReleases: number - }>({ - namespace: 'ReleasesUpsellLimits', - dependencies: [activeReleases], - }) - - if (!_cachedState) return - - const {cacheExpiresAt, cachedValue} = _cachedState - const now = Date.now() - - const datasetLimit = cachedValue?.datasetReleaseLimit - const orgLimit = cachedValue?.orgActiveReleaseLimit - - if ( - (datasetLimit !== null && activeReleases.length === datasetLimit) || - (orgLimit !== null && activeReleases.length === orgLimit) - ) { - console.log('At limit (dataset or org), keeping cache indefinitely.') - return - } - - if (cacheExpiresAt !== null && now >= cacheExpiresAt) { - console.log('Cache TTL expired, marking cache as expired...') - resourceCache.set({ - namespace: 'ReleasesUpsellLimits', - dependencies: [activeReleases], - value: {..._cachedState, expired: true}, - }) - } - }, [activeReleases, resourceCache]) - const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { let limits = releaseLimits + const activeReleasesCount = activeReleases?.length || 0 - const _cachedState = resourceCache.get<{ - cacheExpiresAt: number | null - cachedValue: any - expired?: boolean - }>({ - namespace: 'ReleasesUpsellLimits', - dependencies: [activeReleases], - }) - - const now = Date.now() - - if ( - _cachedState && - _cachedState.cachedValue && - !_cachedState.expired && - now < _cachedState.cacheExpiresAt! - ) { - console.log('Using cached limits from ResourceCache') - limits = _cachedState.cachedValue - } - - const datasetLimit = _cachedState?.cachedValue?.datasetReleaseLimit - const orgLimit = _cachedState?.cachedValue?.orgActiveReleaseLimit - - if ( - (datasetLimit !== null && activeReleases.length === datasetLimit) || - (orgLimit !== null && activeReleases.length === orgLimit) - ) { - console.log('At limit (dataset or org), NOT fetching new data.') - limits = _cachedState?.cachedValue - } - - if (!limits || _cachedState?.expired) { - console.log('Cache is expired or missing. Fetching new data...') + if (!limits) { + console.log('Cache expired or activeReleases changed. Fetching new data...') try { limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) @@ -342,11 +255,12 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { dependencies: [activeReleases], namespace: 'ReleasesUpsellLimits', value: { - cacheExpiresAt: Date.now() + CACHE_TTL_MS, cachedValue: limits, activeReleases: activeReleasesCount, }, }) + + cacheTrigger$.next(activeReleasesCount) } catch (error) { console.error('Error fetching release limits:', error) return @@ -377,7 +291,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { cb() }, - [activeReleases, activeReleasesCount, handleOpenDialog, releaseLimits, resourceCache], + [activeReleases, handleOpenDialog, releaseLimits, resourceCache], ) const onReleaseLimitReached = useCallback( From 010ec5f376592126b0388a3845a83377ab6d236e Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 14:20:37 +0000 Subject: [PATCH 05/38] chore: types --- .../upsell/ReleasesUpsellProvider.tsx | 82 ++-------------- .../releases/store/useReleaseLimitsStore.ts | 96 +++++++++++++++++++ 2 files changed, 104 insertions(+), 74 deletions(-) create mode 100644 packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index eb3bbd73354..65ca25c151f 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,22 +1,11 @@ import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' -import {useObservable} from 'react-rx' -import {BehaviorSubject, firstValueFrom, merge, of, timer} from 'rxjs' -import { - delay, - distinctUntilChanged, - map, - shareReplay, - startWith, - switchMap, - take, - tap, -} from 'rxjs/operators' +import {firstValueFrom, of} from 'rxjs' +import {delay, take, tap} from 'rxjs/operators' import {ReleasesUpsellContext} from 'sanity/_singletons' import {useClient, useFeatureEnabled, useProjectId} from '../../../hooks' -import {useResourceCache} from '../../../store/_legacy/ResourceCacheProvider' import { UpsellDialogDismissed, UpsellDialogLearnMoreCtaClicked, @@ -27,6 +16,7 @@ import {TEMPLATE_OPTIONS} from '../../../studio/upsell/constants' import {type UpsellData} from '../../../studio/upsell/types' import {UpsellDialog} from '../../../studio/upsell/UpsellDialog' import {useActiveReleases} from '../../store/useActiveReleases' +import {useReleaseLimitsStore} from '../../store/useReleaseLimitsStore' import {type ReleasesUpsellContextValue} from './types' type ReleaseLimits = { @@ -52,7 +42,7 @@ const BASE_URL = 'www.sanity.io' // Date when the change from array to object in the data returned was introduced. const API_VERSION = '2024-04-19' -const fetchReleasesLimits = () => +export const fetchReleasesLimits = () => of({ orgActiveReleaseCount: 10, orgActiveReleaseLimit: 20, @@ -66,9 +56,6 @@ const fetchReleasesLimits = () => delay(3000), ) -const cacheTrigger$ = new BehaviorSubject(null) -const CACHE_TTL_MS = 15000 // 1 minute - /** * @beta * @hidden @@ -192,56 +179,11 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { }) }, [telemetry]) - const resourceCache = useResourceCache() - - const cache$ = useMemo( - () => - cacheTrigger$.pipe( - distinctUntilChanged(), - switchMap((activeReleasesCount) => { - if (activeReleasesCount === null) return of(null) - - const cachedState = resourceCache.get<{ - cachedValue: any - activeReleases: number - }>({ - namespace: 'ReleasesUpsellLimits', - dependencies: [activeReleases], - }) - - if (cachedState) { - const {cachedValue, activeReleases: cachedReleases} = cachedState - - if (cachedReleases === activeReleasesCount) { - console.log('Using cached value.') - - return merge( - of(cachedValue), - timer(CACHE_TTL_MS).pipe( - map(() => { - console.log('TTL expired, emitting null.') - cacheTrigger$.next(null) - return null - }), - ), - ) - } - } - - return of(null) - }), - startWith(null), - shareReplay({bufferSize: 1, refCount: true}), - ), - [activeReleases, resourceCache], - ) - - const releaseLimits = useObservable(cache$, null) + const [releaseLimits, setReleaseLimits] = useReleaseLimitsStore() const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { let limits = releaseLimits - const activeReleasesCount = activeReleases?.length || 0 if (!limits) { console.log('Cache expired or activeReleases changed. Fetching new data...') @@ -251,16 +193,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { console.log('Received first API response', limits) - resourceCache.set({ - dependencies: [activeReleases], - namespace: 'ReleasesUpsellLimits', - value: { - cachedValue: limits, - activeReleases: activeReleasesCount, - }, - }) - - cacheTrigger$.next(activeReleasesCount) + setReleaseLimits(limits) } catch (error) { console.error('Error fetching release limits:', error) return @@ -276,6 +209,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const {datasetReleaseLimit, orgActiveReleaseCount, orgActiveReleaseLimit} = limits + const activeReleasesCount = activeReleases?.length || 0 const shouldShowDialog = activeReleasesCount >= datasetReleaseLimit || (orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit) @@ -291,7 +225,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { cb() }, - [activeReleases, handleOpenDialog, releaseLimits, resourceCache], + [activeReleases?.length, handleOpenDialog, releaseLimits, setReleaseLimits], ) const onReleaseLimitReached = useCallback( diff --git a/packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts b/packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts new file mode 100644 index 00000000000..581d0527037 --- /dev/null +++ b/packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts @@ -0,0 +1,96 @@ +import {useMemo} from 'react' +import {useObservable} from 'react-rx' +import { + BehaviorSubject, + distinctUntilChanged, + map, + merge, + shareReplay, + startWith, + switchMap, +} from 'rxjs' +import {of} from 'rxjs/internal/observable/of' +import {timer} from 'rxjs/internal/observable/timer' + +import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {useActiveReleases} from './useActiveReleases' + +interface ReleaseLimits { + datasetReleaseLimit: number + orgActiveReleaseLimit: number | null + orgActiveReleaseCount: number +} + +type UseReleaseLimitsStoreReturn = [ + cache: ReleaseLimits | null, + setReleasesUpsellStoreValue: (value: ReleaseLimits) => void, +] + +const cacheTrigger$ = new BehaviorSubject(null) +const CACHE_TTL_MS = 15000 + +export const useReleaseLimitsStore = (): UseReleaseLimitsStoreReturn => { + const resourceCache = useResourceCache() + const {data: activeReleases} = useActiveReleases() + + const cache$ = useMemo( + () => + cacheTrigger$.pipe( + distinctUntilChanged(), + switchMap((activeReleasesCount) => { + if (activeReleasesCount === null) return of(null) + + const cachedState = resourceCache.get<{ + cachedValue: ReleaseLimits + activeReleases: number + }>({ + namespace: 'ReleasesUpsellLimits', + dependencies: [activeReleases], + }) + + if (cachedState) { + const {cachedValue, activeReleases: cachedReleases} = cachedState + + if (cachedReleases === activeReleasesCount) { + console.log('Using cached value.') + + return merge( + of(cachedValue), + timer(CACHE_TTL_MS).pipe( + map(() => { + console.log('TTL expired, emitting null.') + cacheTrigger$.next(null) + return null + }), + ), + ) + } + } + + return of(null) + }), + startWith(null), + shareReplay({bufferSize: 1, refCount: true}), + ), + [activeReleases, resourceCache], + ) + + const setReleasesUpsellStoreValue = (value: ReleaseLimits) => { + const activeReleasesCount = activeReleases?.length || 0 + + resourceCache.set({ + dependencies: [activeReleases], + namespace: 'ReleasesUpsellLimits', + value: { + cachedValue: value, + activeReleases: activeReleasesCount, + }, + }) + + cacheTrigger$.next(activeReleasesCount) + } + + const cache = useObservable(cache$, null) + + return [cache, setReleasesUpsellStoreValue] +} From 2e5e89f13c16ecf95f6c35682f81d7de65b665b5 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 15:44:09 +0000 Subject: [PATCH 06/38] refactor: splitting separate stores for limits and count --- .../upsell/ReleasesUpsellProvider.tsx | 38 +++---------- ...tsStore.ts => useOrgActiveReleaseCount.ts} | 56 ++++++++++++------- .../core/releases/store/useReleaseLimits.ts | 48 ++++++++++++++++ .../ReleaseRevertButton.tsx | 25 +++++++-- 4 files changed, 113 insertions(+), 54 deletions(-) rename packages/sanity/src/core/releases/store/{useReleaseLimitsStore.ts => useOrgActiveReleaseCount.ts} (63%) create mode 100644 packages/sanity/src/core/releases/store/useReleaseLimits.ts diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 65ca25c151f..7b443e67468 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,8 +1,8 @@ import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' -import {firstValueFrom, of} from 'rxjs' -import {delay, take, tap} from 'rxjs/operators' +import {of} from 'rxjs' +import {delay, tap} from 'rxjs/operators' import {ReleasesUpsellContext} from 'sanity/_singletons' import {useClient, useFeatureEnabled, useProjectId} from '../../../hooks' @@ -16,7 +16,7 @@ import {TEMPLATE_OPTIONS} from '../../../studio/upsell/constants' import {type UpsellData} from '../../../studio/upsell/types' import {UpsellDialog} from '../../../studio/upsell/UpsellDialog' import {useActiveReleases} from '../../store/useActiveReleases' -import {useReleaseLimitsStore} from '../../store/useReleaseLimitsStore' +import {useOrgActiveReleaseCount} from '../../store/useOrgActiveReleaseCount' import {type ReleasesUpsellContextValue} from './types' type ReleaseLimits = { @@ -179,33 +179,13 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { }) }, [telemetry]) - const [releaseLimits, setReleaseLimits] = useReleaseLimitsStore() + const getActiveReleasesCountStoreValue = useOrgActiveReleaseCount() const guardWithReleaseLimitUpsell = useCallback( - async (cb: () => void, throwError: boolean = false) => { - let limits = releaseLimits + async (cb: (limits: any | undefined) => void, throwError: boolean = false) => { + const limits = await getActiveReleasesCountStoreValue() - if (!limits) { - console.log('Cache expired or activeReleases changed. Fetching new data...') - - try { - limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) - - console.log('Received first API response', limits) - - setReleaseLimits(limits) - } catch (error) { - console.error('Error fetching release limits:', error) - return - } - } - - if (!limits) { - console.warn('Fetch was triggered, but still no limits found. Skipping callback execution.') - return - } - - if ('error' in limits) return cb() + if ('error' in limits) return cb(undefined) const {datasetReleaseLimit, orgActiveReleaseCount, orgActiveReleaseLimit} = limits @@ -223,9 +203,9 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { return } - cb() + cb(limits) }, - [activeReleases?.length, handleOpenDialog, releaseLimits, setReleaseLimits], + [activeReleases?.length, getActiveReleasesCountStoreValue, handleOpenDialog], ) const onReleaseLimitReached = useCallback( diff --git a/packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts similarity index 63% rename from packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts rename to packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 581d0527037..f7ced278f93 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimitsStore.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -3,16 +3,19 @@ import {useObservable} from 'react-rx' import { BehaviorSubject, distinctUntilChanged, + firstValueFrom, map, merge, shareReplay, startWith, switchMap, + take, } from 'rxjs' import {of} from 'rxjs/internal/observable/of' import {timer} from 'rxjs/internal/observable/timer' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' import {useActiveReleases} from './useActiveReleases' interface ReleaseLimits { @@ -21,15 +24,12 @@ interface ReleaseLimits { orgActiveReleaseCount: number } -type UseReleaseLimitsStoreReturn = [ - cache: ReleaseLimits | null, - setReleasesUpsellStoreValue: (value: ReleaseLimits) => void, -] +type UseOrgActiveReleaseCountReturn = () => Promise const cacheTrigger$ = new BehaviorSubject(null) const CACHE_TTL_MS = 15000 -export const useReleaseLimitsStore = (): UseReleaseLimitsStoreReturn => { +export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() @@ -44,7 +44,7 @@ export const useReleaseLimitsStore = (): UseReleaseLimitsStoreReturn => { cachedValue: ReleaseLimits activeReleases: number }>({ - namespace: 'ReleasesUpsellLimits', + namespace: 'OrgActiveReleasesCount', dependencies: [activeReleases], }) @@ -75,22 +75,38 @@ export const useReleaseLimitsStore = (): UseReleaseLimitsStoreReturn => { [activeReleases, resourceCache], ) - const setReleasesUpsellStoreValue = (value: ReleaseLimits) => { - const activeReleasesCount = activeReleases?.length || 0 + const cache = useObservable(cache$, null) - resourceCache.set({ - dependencies: [activeReleases], - namespace: 'ReleasesUpsellLimits', - value: { - cachedValue: value, - activeReleases: activeReleasesCount, - }, - }) + const getOrgActiveReleasesCountStoreValue = async () => { + let limits = cache - cacheTrigger$.next(activeReleasesCount) - } + if (limits) return limits - const cache = useObservable(cache$, null) + console.log('Cache expired or activeReleases changed. Fetching new data...') + + try { + limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) + + console.log('Received first API response', limits) + + const activeReleasesCount = activeReleases?.length || 0 + + resourceCache.set({ + dependencies: [activeReleases], + namespace: 'OrgActiveReleasesCount', + value: { + cachedValue: limits, + activeReleases: activeReleasesCount, + }, + }) + + cacheTrigger$.next(activeReleasesCount) + + return limits + } catch (error) { + console.error('Error fetching release limits:', error) + } + } - return [cache, setReleasesUpsellStoreValue] + return getOrgActiveReleasesCountStoreValue } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts new file mode 100644 index 00000000000..f128c2b0f9d --- /dev/null +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -0,0 +1,48 @@ +import {useMemo} from 'react' +import {useObservable} from 'react-rx' +import {defer, map, shareReplay} from 'rxjs' + +import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' + +// ✅ Define the return type +interface ReleaseLimits { + datasetReleaseLimit: number + orgActiveReleaseLimit: number | null +} + +// ✅ Hook that fetches once and reuses the same data forever +export const useReleaseLimits = () => { + const resourceCache = useResourceCache() + + const releaseLimits$ = useMemo(() => { + return defer(() => { + const cachedState = resourceCache.get({ + namespace: 'ReleaseLimits', + dependencies: [], + }) + + if (cachedState) { + console.log('Using cached ReleaseLimits') + return of(cachedState) // ✅ Reuse existing cached value + } + + console.log('Fetching ReleaseLimits for the first time...') + return fetchReleasesLimits().pipe( + map(({datasetReleaseLimit, orgActiveReleaseLimit}) => { + const limits: ReleaseLimits = {datasetReleaseLimit, orgActiveReleaseLimit} + + resourceCache.set({ + namespace: 'ReleaseLimits', + dependencies: [], + value: limits, + }) + + return limits + }), + ) + }).pipe(shareReplay({bufferSize: 1, refCount: false})) // ✅ Ensures all subscribers get the first fetched value + }, [resourceCache]) + + return useObservable(releaseLimits$, null) +} diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx index d37a71aaa5e..5f66855fced 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx @@ -1,7 +1,7 @@ import {RestoreIcon} from '@sanity/icons' import {useTelemetry} from '@sanity/telemetry/react' import {Box, Card, Checkbox, Flex, Text, useToast} from '@sanity/ui' -import {useCallback, useState} from 'react' +import {useCallback, useEffect, useState} from 'react' import {useRouter} from 'sanity/router' import {Button} from '../../../../../../ui-components/button/Button' @@ -212,9 +212,6 @@ const ConfirmReleaseDialog = ({ ) } -// TODO: This is going to be disabled until we have the proper "releases plus" flag -const isRevertEnabled = false - export const ReleaseRevertButton = ({ release, documents, @@ -229,7 +226,25 @@ export const ReleaseRevertButton = ({ [guardWithReleaseLimitUpsell], ) - if (!isRevertEnabled) return null + const [isReleasesPlus, setIsReleasesPlug] = useState(undefined) + const [isFetchingLimits, setIsFetchingLimits] = useState(false) + + // const getLimits = useReleaseLimitsStore() + + // useEffect(() => { + // if (isReleasesPlus !== undefined || isFetchingLimits) return + + // setIsFetchingLimits(true) + // getLimits().then((limits) => { + // setIsReleasesPlug(limits.orgActiveReleaseLimit > 2) + // }) + // }, [getLimits, isFetchingLimits, isReleasesPlus]) + + useEffect(() => { + guardWithReleaseLimitUpsell((limits) => setIsReleasesPlug(limits.orgActiveReleaseLimit > 2)) + }, [guardWithReleaseLimitUpsell]) + + if (!isReleasesPlus) return null return ( <> From 2b8b30cc61dcd07e447dfa572e08c2ff618cbac2 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 15:58:21 +0000 Subject: [PATCH 07/38] refactor: further splitting of the stores --- .../upsell/ReleasesUpsellProvider.tsx | 27 +++++++--- .../core/releases/store/useReleaseLimits.ts | 50 ++++++++++++++++--- .../ReleaseRevertButton.tsx | 25 ++++------ 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 7b443e67468..e1142c51218 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -17,6 +17,7 @@ import {type UpsellData} from '../../../studio/upsell/types' import {UpsellDialog} from '../../../studio/upsell/UpsellDialog' import {useActiveReleases} from '../../store/useActiveReleases' import {useOrgActiveReleaseCount} from '../../store/useOrgActiveReleaseCount' +import {useReleaseLimits} from '../../store/useReleaseLimits' import {type ReleasesUpsellContextValue} from './types' type ReleaseLimits = { @@ -179,33 +180,47 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { }) }, [telemetry]) + const fetchReleaseLimits = useReleaseLimits() const getActiveReleasesCountStoreValue = useOrgActiveReleaseCount() const guardWithReleaseLimitUpsell = useCallback( async (cb: (limits: any | undefined) => void, throwError: boolean = false) => { - const limits = await getActiveReleasesCountStoreValue() + console.log('Guard called, triggering cache fetches...') - if ('error' in limits) return cb(undefined) + // ✅ Fetch both values **only when guard is called** + const [limits, orgActiveReleaseCount] = await Promise.all([ + fetchReleaseLimits(), // ✅ Triggers fetching if needed + getActiveReleasesCountStoreValue(), // ✅ Fetches count with TTL handling + ]) - const {datasetReleaseLimit, orgActiveReleaseCount, orgActiveReleaseLimit} = limits + if (!limits || orgActiveReleaseCount === null) { + console.warn('Cache expired or data missing. Skipping callback execution.') + return + } + const {datasetReleaseLimit, orgActiveReleaseLimit} = limits const activeReleasesCount = activeReleases?.length || 0 + const shouldShowDialog = activeReleasesCount >= datasetReleaseLimit || (orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit) if (shouldShowDialog) { handleOpenDialog() - if (throwError) { throw new StudioReleaseLimitExceededError() } return } - cb(limits) + cb({datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount}) }, - [activeReleases?.length, getActiveReleasesCountStoreValue, handleOpenDialog], + [ + activeReleases?.length, + getActiveReleasesCountStoreValue, + handleOpenDialog, + fetchReleaseLimits, + ], ) const onReleaseLimitReached = useCallback( diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index f128c2b0f9d..315b7cfd0a6 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,20 +1,34 @@ import {useMemo} from 'react' -import {useObservable} from 'react-rx' -import {defer, map, shareReplay} from 'rxjs' +import {BehaviorSubject, defer, delay, firstValueFrom, map, of, shareReplay, tap} from 'rxjs' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' +export const fetchReleasesLimits = () => + of({ + orgActiveReleaseCount: 10, + orgActiveReleaseLimit: 20, + datasetReleaseLimit: 6, + + // orgActiveReleaseCount: 6, + // orgActiveReleaseLimit: 6, + // datasetReleaseLimit: 10, + }).pipe( + tap(() => console.log('SEE THIS ONLY ONCE fetchReleasesLimits')), + delay(3000), + ) // ✅ Define the return type interface ReleaseLimits { datasetReleaseLimit: number orgActiveReleaseLimit: number | null } -// ✅ Hook that fetches once and reuses the same data forever +// ✅ BehaviorSubject to **store** the cached value +const releaseLimitsSubject = new BehaviorSubject(null) + export const useReleaseLimits = () => { const resourceCache = useResourceCache() + // ✅ Observable that fetches **only if needed** const releaseLimits$ = useMemo(() => { return defer(() => { const cachedState = resourceCache.get({ @@ -24,25 +38,45 @@ export const useReleaseLimits = () => { if (cachedState) { console.log('Using cached ReleaseLimits') - return of(cachedState) // ✅ Reuse existing cached value + return of(cachedState) // ✅ Use existing cache } - console.log('Fetching ReleaseLimits for the first time...') + console.log('Fetching ReleaseLimits...') return fetchReleasesLimits().pipe( map(({datasetReleaseLimit, orgActiveReleaseLimit}) => { const limits: ReleaseLimits = {datasetReleaseLimit, orgActiveReleaseLimit} + // ✅ Store in cache **for future calls** resourceCache.set({ namespace: 'ReleaseLimits', dependencies: [], value: limits, }) + // ✅ Store in BehaviorSubject so it never fetches again + releaseLimitsSubject.next(limits) + return limits }), ) - }).pipe(shareReplay({bufferSize: 1, refCount: false})) // ✅ Ensures all subscribers get the first fetched value + }).pipe( + shareReplay({bufferSize: 1, refCount: true}), // ✅ Ensures all subscribers share the same result + ) }, [resourceCache]) - return useObservable(releaseLimits$, null) + // ✅ Function to **trigger the fetch only once** + const fetchReleaseLimits = async () => { + // ✅ Return cached value if already present + const existingValue = releaseLimitsSubject.getValue() + if (existingValue) { + console.log('Returning already cached ReleaseLimits') + return existingValue + } + + // ✅ Otherwise, trigger a new fetch + console.log('Triggering new fetch for ReleaseLimits...') + return firstValueFrom(releaseLimits$) + } + + return fetchReleaseLimits } diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx index 5f66855fced..7d5f1aad582 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx @@ -12,6 +12,7 @@ import {useReleasesUpsell} from '../../../../contexts/upsell/useReleasesUpsell' import {releasesLocaleNamespace} from '../../../../i18n' import {isReleaseLimitError} from '../../../../store/isReleaseLimitError' import {type ReleaseDocument} from '../../../../store/types' +import {useReleaseLimits} from '../../../../store/useReleaseLimits' import {useReleaseOperations} from '../../../../store/useReleaseOperations' import {createReleaseId} from '../../../../util/createReleaseId' import {getReleaseIdFromReleaseDocumentId} from '../../../../util/getReleaseIdFromReleaseDocumentId' @@ -226,23 +227,19 @@ export const ReleaseRevertButton = ({ [guardWithReleaseLimitUpsell], ) - const [isReleasesPlus, setIsReleasesPlug] = useState(undefined) - const [isFetchingLimits, setIsFetchingLimits] = useState(false) - - // const getLimits = useReleaseLimitsStore() - - // useEffect(() => { - // if (isReleasesPlus !== undefined || isFetchingLimits) return + const getReleaseLimits = useReleaseLimits() - // setIsFetchingLimits(true) - // getLimits().then((limits) => { - // setIsReleasesPlug(limits.orgActiveReleaseLimit > 2) - // }) - // }, [getLimits, isFetchingLimits, isReleasesPlus]) + const [isReleasesPlus, setIsReleasesPlus] = useState(undefined) + const [isFetchingLimits, setIsFetchingLimits] = useState(false) useEffect(() => { - guardWithReleaseLimitUpsell((limits) => setIsReleasesPlug(limits.orgActiveReleaseLimit > 2)) - }, [guardWithReleaseLimitUpsell]) + if (isReleasesPlus !== undefined || isFetchingLimits) return + + setIsFetchingLimits(true) + getReleaseLimits().then((limits) => { + setIsReleasesPlus(limits.orgActiveReleaseLimit > 2) + }) + }, [getReleaseLimits, isFetchingLimits, isReleasesPlus]) if (!isReleasesPlus) return null From 113bf18644226c56428781b4f8532041d6c99f68 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 16:01:52 +0000 Subject: [PATCH 08/38] refactor: minor renaming --- .../contexts/upsell/ReleasesUpsellProvider.tsx | 5 ++--- .../src/core/releases/store/useReleaseLimits.ts | 13 ++----------- .../ReleaseRevertButton/ReleaseRevertButton.tsx | 8 ++++---- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index e1142c51218..169625d3418 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -187,10 +187,9 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { async (cb: (limits: any | undefined) => void, throwError: boolean = false) => { console.log('Guard called, triggering cache fetches...') - // ✅ Fetch both values **only when guard is called** const [limits, orgActiveReleaseCount] = await Promise.all([ - fetchReleaseLimits(), // ✅ Triggers fetching if needed - getActiveReleasesCountStoreValue(), // ✅ Fetches count with TTL handling + fetchReleaseLimits(), + getActiveReleasesCountStoreValue(), ]) if (!limits || orgActiveReleaseCount === null) { diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 315b7cfd0a6..368c958f4be 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -16,19 +16,17 @@ export const fetchReleasesLimits = () => tap(() => console.log('SEE THIS ONLY ONCE fetchReleasesLimits')), delay(3000), ) -// ✅ Define the return type + interface ReleaseLimits { datasetReleaseLimit: number orgActiveReleaseLimit: number | null } -// ✅ BehaviorSubject to **store** the cached value const releaseLimitsSubject = new BehaviorSubject(null) export const useReleaseLimits = () => { const resourceCache = useResourceCache() - // ✅ Observable that fetches **only if needed** const releaseLimits$ = useMemo(() => { return defer(() => { const cachedState = resourceCache.get({ @@ -46,34 +44,27 @@ export const useReleaseLimits = () => { map(({datasetReleaseLimit, orgActiveReleaseLimit}) => { const limits: ReleaseLimits = {datasetReleaseLimit, orgActiveReleaseLimit} - // ✅ Store in cache **for future calls** resourceCache.set({ namespace: 'ReleaseLimits', dependencies: [], value: limits, }) - // ✅ Store in BehaviorSubject so it never fetches again releaseLimitsSubject.next(limits) return limits }), ) - }).pipe( - shareReplay({bufferSize: 1, refCount: true}), // ✅ Ensures all subscribers share the same result - ) + }).pipe(shareReplay({bufferSize: 1, refCount: true})) }, [resourceCache]) - // ✅ Function to **trigger the fetch only once** const fetchReleaseLimits = async () => { - // ✅ Return cached value if already present const existingValue = releaseLimitsSubject.getValue() if (existingValue) { console.log('Returning already cached ReleaseLimits') return existingValue } - // ✅ Otherwise, trigger a new fetch console.log('Triggering new fetch for ReleaseLimits...') return firstValueFrom(releaseLimits$) } diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx index 7d5f1aad582..764b3bf0d1b 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx @@ -230,16 +230,16 @@ export const ReleaseRevertButton = ({ const getReleaseLimits = useReleaseLimits() const [isReleasesPlus, setIsReleasesPlus] = useState(undefined) - const [isFetchingLimits, setIsFetchingLimits] = useState(false) + const [hasFetchedLimits, setHasFetchedLimits] = useState(false) useEffect(() => { - if (isReleasesPlus !== undefined || isFetchingLimits) return + if (isReleasesPlus !== undefined || hasFetchedLimits) return - setIsFetchingLimits(true) + setHasFetchedLimits(true) getReleaseLimits().then((limits) => { setIsReleasesPlus(limits.orgActiveReleaseLimit > 2) }) - }, [getReleaseLimits, isFetchingLimits, isReleasesPlus]) + }, [getReleaseLimits, hasFetchedLimits, isReleasesPlus]) if (!isReleasesPlus) return null From 92b47962eb79001537ff70c16e5aab0d183e5fc3 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 16:11:15 +0000 Subject: [PATCH 09/38] refactor: useOrgActiveReleaseCount only stores the count, nothing else --- .../releases/store/useOrgActiveReleaseCount.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index f7ced278f93..07f7627f347 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -41,7 +41,7 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { if (activeReleasesCount === null) return of(null) const cachedState = resourceCache.get<{ - cachedValue: ReleaseLimits + cachedValue: number activeReleases: number }>({ namespace: 'OrgActiveReleasesCount', @@ -78,16 +78,17 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { const cache = useObservable(cache$, null) const getOrgActiveReleasesCountStoreValue = async () => { - let limits = cache + let count = cache - if (limits) return limits + if (count) return count console.log('Cache expired or activeReleases changed. Fetching new data...') try { - limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) + const limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) + count = limits.orgActiveReleaseCount - console.log('Received first API response', limits) + console.log('Received first API response', count) const activeReleasesCount = activeReleases?.length || 0 @@ -95,14 +96,14 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { dependencies: [activeReleases], namespace: 'OrgActiveReleasesCount', value: { - cachedValue: limits, + cachedValue: count, activeReleases: activeReleasesCount, }, }) cacheTrigger$.next(activeReleasesCount) - return limits + return count } catch (error) { console.error('Error fetching release limits:', error) } From 9436e94545d47ebbeb0b78631481ae66cc784c9f Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 17:21:17 +0000 Subject: [PATCH 10/38] fix: using a shared network request in guard --- .../upsell/ReleasesUpsellProvider.tsx | 76 +++++++++++++------ .../store/useOrgActiveReleaseCount.ts | 61 +++++++-------- .../core/releases/store/useReleaseLimits.ts | 25 +++++- .../ReleaseRevertButton.tsx | 6 +- 4 files changed, 105 insertions(+), 63 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 169625d3418..09d1e8bca94 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,7 +1,7 @@ import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' -import {of} from 'rxjs' +import {firstValueFrom, of} from 'rxjs' import {delay, tap} from 'rxjs/operators' import {ReleasesUpsellContext} from 'sanity/_singletons' @@ -180,45 +180,71 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { }) }, [telemetry]) - const fetchReleaseLimits = useReleaseLimits() - const getActiveReleasesCountStoreValue = useOrgActiveReleaseCount() + const {getReleaseLimits, setLimitsManually} = useReleaseLimits() + const {getOrgActiveReleaseCount, setOrgActiveReleaseCountManually} = useOrgActiveReleaseCount() const guardWithReleaseLimitUpsell = useCallback( async (cb: (limits: any | undefined) => void, throwError: boolean = false) => { - console.log('Guard called, triggering cache fetches...') + console.log('Guard called, checking caches...') - const [limits, orgActiveReleaseCount] = await Promise.all([ - fetchReleaseLimits(), - getActiveReleasesCountStoreValue(), - ]) + const existingLimits = getReleaseLimits() + const existingOrgActiveReleaseCount = getOrgActiveReleaseCount() - if (!limits || orgActiveReleaseCount === null) { - console.warn('Cache expired or data missing. Skipping callback execution.') - return + console.log({existingLimits, existingOrgActiveReleaseCount}) + + if (existingLimits && existingOrgActiveReleaseCount !== null) { + console.log('Both caches valid, skipping fetch.') + return cb({ + datasetReleaseLimit: existingLimits.datasetReleaseLimit, + orgActiveReleaseLimit: existingLimits.orgActiveReleaseLimit, + orgActiveReleaseCount: existingOrgActiveReleaseCount, + }) } - const {datasetReleaseLimit, orgActiveReleaseLimit} = limits - const activeReleasesCount = activeReleases?.length || 0 + console.log('Fetching new data since cache is missing or expired...') - const shouldShowDialog = - activeReleasesCount >= datasetReleaseLimit || - (orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit) + try { + const limits = await firstValueFrom(fetchReleasesLimits()) // ✅ Fetch API once - if (shouldShowDialog) { - handleOpenDialog() - if (throwError) { - throw new StudioReleaseLimitExceededError() + if (!existingLimits) { + console.log('setting release limits', {limits}) + setLimitsManually({ + datasetReleaseLimit: limits.datasetReleaseLimit, + orgActiveReleaseLimit: limits.orgActiveReleaseLimit, + }) + } + + if (existingOrgActiveReleaseCount === null) { + setOrgActiveReleaseCountManually(limits.orgActiveReleaseCount) } - return - } - cb({datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount}) + const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = limits + const activeReleasesCount = activeReleases?.length || 0 + + const shouldShowDialog = + activeReleasesCount >= datasetReleaseLimit || + (orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit) + + if (shouldShowDialog) { + handleOpenDialog() + if (throwError) { + throw new StudioReleaseLimitExceededError() + } + return + } + + cb({datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount}) + } catch (error) { + console.error('Error fetching release limits:', error) + } }, [ activeReleases?.length, - getActiveReleasesCountStoreValue, handleOpenDialog, - fetchReleaseLimits, + getReleaseLimits, + setLimitsManually, + getOrgActiveReleaseCount, + setOrgActiveReleaseCountManually, ], ) diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 07f7627f347..aaf8ef05849 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,21 +1,17 @@ import {useMemo} from 'react' -import {useObservable} from 'react-rx' import { BehaviorSubject, distinctUntilChanged, - firstValueFrom, map, merge, shareReplay, startWith, switchMap, - take, } from 'rxjs' import {of} from 'rxjs/internal/observable/of' import {timer} from 'rxjs/internal/observable/timer' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' import {useActiveReleases} from './useActiveReleases' interface ReleaseLimits { @@ -33,7 +29,7 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() - const cache$ = useMemo( + useMemo( () => cacheTrigger$.pipe( distinctUntilChanged(), @@ -59,6 +55,12 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { timer(CACHE_TTL_MS).pipe( map(() => { console.log('TTL expired, emitting null.') + resourceCache.set({ + namespace: 'OrgActiveReleasesCount', + dependencies: [activeReleases], + value: null, // 🚨 Force cache to be removed + }) + cacheTrigger$.next(null) return null }), @@ -75,39 +77,30 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { [activeReleases, resourceCache], ) - const cache = useObservable(cache$, null) - - const getOrgActiveReleasesCountStoreValue = async () => { - let count = cache - - if (count) return count - - console.log('Cache expired or activeReleases changed. Fetching new data...') + const setOrgActiveReleaseCountManually = (count: number) => { + const activeReleasesCount = activeReleases?.length || 0 - try { - const limits = await firstValueFrom(fetchReleasesLimits().pipe(take(1))) - count = limits.orgActiveReleaseCount + console.log('Storing orgActiveReleaseCount...') + resourceCache.set({ + namespace: 'OrgActiveReleasesCount', + dependencies: [activeReleases], + value: { + cachedValue: count, + activeReleases: activeReleasesCount, + }, + }) - console.log('Received first API response', count) - - const activeReleasesCount = activeReleases?.length || 0 - - resourceCache.set({ - dependencies: [activeReleases], - namespace: 'OrgActiveReleasesCount', - value: { - cachedValue: count, - activeReleases: activeReleasesCount, - }, - }) + cacheTrigger$.next(activeReleasesCount) + } - cacheTrigger$.next(activeReleasesCount) + const getOrgActiveReleaseCount = () => { + const cachedState = resourceCache.get<{cachedValue: number; activeReleases: number}>({ + namespace: 'OrgActiveReleasesCount', + dependencies: [activeReleases], + }) - return count - } catch (error) { - console.error('Error fetching release limits:', error) - } + return cachedState?.cachedValue ?? null } - return getOrgActiveReleasesCountStoreValue + return {setOrgActiveReleaseCountManually, getOrgActiveReleaseCount} } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 368c958f4be..8b806a68f18 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -69,5 +69,28 @@ export const useReleaseLimits = () => { return firstValueFrom(releaseLimits$) } - return fetchReleaseLimits + const setLimitsManually = (limits: ReleaseLimits) => { + console.log('Storing ReleaseLimits...') + + resourceCache.set({ + namespace: 'ReleaseLimits', + dependencies: [], + value: limits, + }) + + releaseLimitsSubject.next(limits) + } + + const getReleaseLimits = () => { + return ( + releaseLimitsSubject.getValue() || + resourceCache.get({ + namespace: 'ReleaseLimits', + dependencies: [], + }) || + null + ) + } + + return {fetchReleaseLimits, setLimitsManually, getReleaseLimits} } diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx index 764b3bf0d1b..6ec48a42711 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx @@ -227,7 +227,7 @@ export const ReleaseRevertButton = ({ [guardWithReleaseLimitUpsell], ) - const getReleaseLimits = useReleaseLimits() + const {fetchReleaseLimits} = useReleaseLimits() const [isReleasesPlus, setIsReleasesPlus] = useState(undefined) const [hasFetchedLimits, setHasFetchedLimits] = useState(false) @@ -236,10 +236,10 @@ export const ReleaseRevertButton = ({ if (isReleasesPlus !== undefined || hasFetchedLimits) return setHasFetchedLimits(true) - getReleaseLimits().then((limits) => { + fetchReleaseLimits().then((limits) => { setIsReleasesPlus(limits.orgActiveReleaseLimit > 2) }) - }, [getReleaseLimits, hasFetchedLimits, isReleasesPlus]) + }, [fetchReleaseLimits, hasFetchedLimits, isReleasesPlus]) if (!isReleasesPlus) return null From 2f5265d3dfd51c2a4ab1957da5c919b95badf308 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Mon, 17 Feb 2025 18:10:40 +0000 Subject: [PATCH 11/38] refactor: tidy and reinstating useObservable --- .../upsell/ReleasesUpsellProvider.tsx | 27 +++++++++------- .../core/releases/hooks/useIsReleasesPlus.ts | 25 +++++++++++++++ .../store/useOrgActiveReleaseCount.ts | 7 +++-- .../core/releases/store/useReleaseLimits.ts | 31 +++++++------------ .../ReleaseRevertButton.tsx | 18 ++--------- 5 files changed, 59 insertions(+), 49 deletions(-) create mode 100644 packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 09d1e8bca94..c6c3cf97e60 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -184,7 +184,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const {getOrgActiveReleaseCount, setOrgActiveReleaseCountManually} = useOrgActiveReleaseCount() const guardWithReleaseLimitUpsell = useCallback( - async (cb: (limits: any | undefined) => void, throwError: boolean = false) => { + async (cb: () => void, throwError: boolean = false) => { console.log('Guard called, checking caches...') const existingLimits = getReleaseLimits() @@ -192,21 +192,17 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { console.log({existingLimits, existingOrgActiveReleaseCount}) - if (existingLimits && existingOrgActiveReleaseCount !== null) { + if (existingLimits !== null && existingOrgActiveReleaseCount !== null) { console.log('Both caches valid, skipping fetch.') - return cb({ - datasetReleaseLimit: existingLimits.datasetReleaseLimit, - orgActiveReleaseLimit: existingLimits.orgActiveReleaseLimit, - orgActiveReleaseCount: existingOrgActiveReleaseCount, - }) + return cb() } console.log('Fetching new data since cache is missing or expired...') try { - const limits = await firstValueFrom(fetchReleasesLimits()) // ✅ Fetch API once + const limits = await firstValueFrom(fetchReleasesLimits()) - if (!existingLimits) { + if (existingLimits === null) { console.log('setting release limits', {limits}) setLimitsManually({ datasetReleaseLimit: limits.datasetReleaseLimit, @@ -221,9 +217,16 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = limits const activeReleasesCount = activeReleases?.length || 0 + const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit + const isCurrentDatasetAtAboveOrgLimit = + orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit + const isOrgAtAboveOrgLimit = + orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit + const shouldShowDialog = - activeReleasesCount >= datasetReleaseLimit || - (orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit) + isCurrentDatasetAtAboveDatasetLimit || + isCurrentDatasetAtAboveOrgLimit || + isOrgAtAboveOrgLimit if (shouldShowDialog) { handleOpenDialog() @@ -233,7 +236,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { return } - cb({datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount}) + cb() } catch (error) { console.error('Error fetching release limits:', error) } diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts new file mode 100644 index 00000000000..dcc8c38c4a4 --- /dev/null +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -0,0 +1,25 @@ +import {useEffect, useState} from 'react' + +import {useReleaseLimits} from '../store/useReleaseLimits' + +const RELEASES_PLUS_LIMIT = 2 + +export const useIsReleasesPlus = () => { + const {fetchReleaseLimits} = useReleaseLimits() + + const [isReleasesPlus, setIsReleasesPlus] = useState(undefined) + const [hasFetchedLimits, setHasFetchedLimits] = useState(false) + + useEffect(() => { + if (isReleasesPlus !== undefined || hasFetchedLimits) return + + setHasFetchedLimits(true) + fetchReleaseLimits() + .then((limits) => + setIsReleasesPlus((limits.orgActiveReleaseLimit || 0) > RELEASES_PLUS_LIMIT), + ) + .catch(() => setIsReleasesPlus(false)) + }, [fetchReleaseLimits, hasFetchedLimits, isReleasesPlus]) + + return isReleasesPlus +} diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index aaf8ef05849..5244f74dc00 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,4 +1,5 @@ import {useMemo} from 'react' +import {useObservable} from 'react-rx' import { BehaviorSubject, distinctUntilChanged, @@ -29,7 +30,7 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() - useMemo( + const cache$ = useMemo( () => cacheTrigger$.pipe( distinctUntilChanged(), @@ -58,7 +59,7 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { resourceCache.set({ namespace: 'OrgActiveReleasesCount', dependencies: [activeReleases], - value: null, // 🚨 Force cache to be removed + value: null, }) cacheTrigger$.next(null) @@ -77,6 +78,8 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { [activeReleases, resourceCache], ) + useObservable(cache$, null) + const setOrgActiveReleaseCountManually = (count: number) => { const activeReleasesCount = activeReleases?.length || 0 diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 8b806a68f18..5e91ed78e9e 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,4 +1,5 @@ import {useMemo} from 'react' +import {useObservable} from 'react-rx' import {BehaviorSubject, defer, delay, firstValueFrom, map, of, shareReplay, tap} from 'rxjs' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' @@ -36,7 +37,7 @@ export const useReleaseLimits = () => { if (cachedState) { console.log('Using cached ReleaseLimits') - return of(cachedState) // ✅ Use existing cache + return of(cachedState) } console.log('Fetching ReleaseLimits...') @@ -58,16 +59,9 @@ export const useReleaseLimits = () => { }).pipe(shareReplay({bufferSize: 1, refCount: true})) }, [resourceCache]) - const fetchReleaseLimits = async () => { - const existingValue = releaseLimitsSubject.getValue() - if (existingValue) { - console.log('Returning already cached ReleaseLimits') - return existingValue - } + const releasesLimit = useObservable(releaseLimitsSubject, null) - console.log('Triggering new fetch for ReleaseLimits...') - return firstValueFrom(releaseLimits$) - } + const fetchReleaseLimits = async () => releasesLimit || firstValueFrom(releaseLimits$) const setLimitsManually = (limits: ReleaseLimits) => { console.log('Storing ReleaseLimits...') @@ -81,16 +75,13 @@ export const useReleaseLimits = () => { releaseLimitsSubject.next(limits) } - const getReleaseLimits = () => { - return ( - releaseLimitsSubject.getValue() || - resourceCache.get({ - namespace: 'ReleaseLimits', - dependencies: [], - }) || - null - ) - } + const getReleaseLimits = () => + releasesLimit || + resourceCache.get({ + namespace: 'ReleaseLimits', + dependencies: [], + }) || + null return {fetchReleaseLimits, setLimitsManually, getReleaseLimits} } diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx index 6ec48a42711..fe0ecfd50d4 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx @@ -1,7 +1,7 @@ import {RestoreIcon} from '@sanity/icons' import {useTelemetry} from '@sanity/telemetry/react' import {Box, Card, Checkbox, Flex, Text, useToast} from '@sanity/ui' -import {useCallback, useEffect, useState} from 'react' +import {useCallback, useState} from 'react' import {useRouter} from 'sanity/router' import {Button} from '../../../../../../ui-components/button/Button' @@ -9,10 +9,10 @@ import {Dialog} from '../../../../../../ui-components/dialog' import {Translate, useTranslation} from '../../../../../i18n' import {RevertRelease} from '../../../../__telemetry__/releases.telemetry' import {useReleasesUpsell} from '../../../../contexts/upsell/useReleasesUpsell' +import {useIsReleasesPlus} from '../../../../hooks/useIsReleasesPlus' import {releasesLocaleNamespace} from '../../../../i18n' import {isReleaseLimitError} from '../../../../store/isReleaseLimitError' import {type ReleaseDocument} from '../../../../store/types' -import {useReleaseLimits} from '../../../../store/useReleaseLimits' import {useReleaseOperations} from '../../../../store/useReleaseOperations' import {createReleaseId} from '../../../../util/createReleaseId' import {getReleaseIdFromReleaseDocumentId} from '../../../../util/getReleaseIdFromReleaseDocumentId' @@ -227,19 +227,7 @@ export const ReleaseRevertButton = ({ [guardWithReleaseLimitUpsell], ) - const {fetchReleaseLimits} = useReleaseLimits() - - const [isReleasesPlus, setIsReleasesPlus] = useState(undefined) - const [hasFetchedLimits, setHasFetchedLimits] = useState(false) - - useEffect(() => { - if (isReleasesPlus !== undefined || hasFetchedLimits) return - - setHasFetchedLimits(true) - fetchReleaseLimits().then((limits) => { - setIsReleasesPlus(limits.orgActiveReleaseLimit > 2) - }) - }, [fetchReleaseLimits, hasFetchedLimits, isReleasesPlus]) + const isReleasesPlus = useIsReleasesPlus() if (!isReleasesPlus) return null From ea698cf0d6c6da08531f6ff66a11616849fd17f2 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 00:15:25 +0000 Subject: [PATCH 12/38] refactor: all working; minor refactor to tidy --- .../upsell/ReleasesUpsellProvider.tsx | 123 ++++++++++-------- .../store/useOrgActiveReleaseCount.ts | 5 +- .../tool/overview/ReleasesOverview.tsx | 26 +++- 3 files changed, 90 insertions(+), 64 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index c6c3cf97e60..48fc22a01b2 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -67,7 +67,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const projectId = useProjectId() const telemetry = useTelemetry() const client = useClient({apiVersion: API_VERSION}) - const [releaseLimit, setReleaseLimit] = useState(undefined) + const [_releaseLimit, setReleaseLimit] = useState(undefined) const {data: activeReleases} = useActiveReleases() const {enabled: isReleasesFeatureEnabled} = useFeatureEnabled('contentReleases') @@ -78,7 +78,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { * there is a limit and the limit is reached or exceeded */ const isAtReleaseLimit = - !isReleasesFeatureEnabled || (releaseLimit && (activeReleases?.length || 0) >= releaseLimit) + !isReleasesFeatureEnabled || (_releaseLimit && (activeReleases?.length || 0) >= _releaseLimit) if (isAtReleaseLimit && upsellData) { return 'upsell' } @@ -86,7 +86,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { return 'disabled' } return 'default' - }, [activeReleases?.length, isReleasesFeatureEnabled, releaseLimit, upsellData]) + }, [activeReleases?.length, isReleasesFeatureEnabled, _releaseLimit, upsellData]) const telemetryLogs = useMemo( (): ReleasesUpsellContextValue['telemetryLogs'] => ({ @@ -170,15 +170,21 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { } }, [client, projectId]) - const handleOpenDialog = useCallback(() => { - setUpsellDialogOpen(true) + const [existingCount, setExistingCount] = useState(0) - telemetry.log(UpsellDialogViewed, { - feature: FEATURE, - type: 'modal', - source: 'navbar', - }) - }, [telemetry]) + const handleOpenDialog = useCallback( + (_existingCount: number = 0) => { + setExistingCount(_existingCount) + setUpsellDialogOpen(true) + + telemetry.log(UpsellDialogViewed, { + feature: FEATURE, + type: 'modal', + source: 'navbar', + }) + }, + [telemetry], + ) const {getReleaseLimits, setLimitsManually} = useReleaseLimits() const {getOrgActiveReleaseCount, setOrgActiveReleaseCountManually} = useOrgActiveReleaseCount() @@ -187,59 +193,62 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { async (cb: () => void, throwError: boolean = false) => { console.log('Guard called, checking caches...') - const existingLimits = getReleaseLimits() - const existingOrgActiveReleaseCount = getOrgActiveReleaseCount() - - console.log({existingLimits, existingOrgActiveReleaseCount}) - - if (existingLimits !== null && existingOrgActiveReleaseCount !== null) { - console.log('Both caches valid, skipping fetch.') - return cb() - } - - console.log('Fetching new data since cache is missing or expired...') + let existingLimits = getReleaseLimits() + let existingOrgActiveReleaseCount = getOrgActiveReleaseCount() - try { - const limits = await firstValueFrom(fetchReleasesLimits()) + if (existingLimits === null || existingOrgActiveReleaseCount === null) { + console.log('Fetching new data since cache is missing or expired...') - if (existingLimits === null) { - console.log('setting release limits', {limits}) - setLimitsManually({ - datasetReleaseLimit: limits.datasetReleaseLimit, - orgActiveReleaseLimit: limits.orgActiveReleaseLimit, - }) - } - - if (existingOrgActiveReleaseCount === null) { - setOrgActiveReleaseCountManually(limits.orgActiveReleaseCount) - } - - const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = limits - const activeReleasesCount = activeReleases?.length || 0 - - const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit - const isCurrentDatasetAtAboveOrgLimit = - orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit - const isOrgAtAboveOrgLimit = - orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit + try { + const limits = await firstValueFrom(fetchReleasesLimits()) + + if (existingLimits === null) { + console.log('setting release limits', {limits}) + setLimitsManually({ + datasetReleaseLimit: limits.datasetReleaseLimit, + orgActiveReleaseLimit: limits.orgActiveReleaseLimit, + }) + } - const shouldShowDialog = - isCurrentDatasetAtAboveDatasetLimit || - isCurrentDatasetAtAboveOrgLimit || - isOrgAtAboveOrgLimit + if (existingOrgActiveReleaseCount === null) + setOrgActiveReleaseCountManually(limits.orgActiveReleaseCount) - if (shouldShowDialog) { - handleOpenDialog() - if (throwError) { - throw new StudioReleaseLimitExceededError() + const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = limits + existingLimits = { + datasetReleaseLimit, + orgActiveReleaseLimit, } - return + existingOrgActiveReleaseCount = orgActiveReleaseCount + } catch (e) { + console.error('Error fetching release limits:', e) } + } - cb() - } catch (error) { - console.error('Error fetching release limits:', error) + const activeReleasesCount = activeReleases?.length || 0 + + const isCurrentDatasetAtAboveDatasetLimit = + activeReleasesCount >= existingLimits.datasetReleaseLimit + const isCurrentDatasetAtAboveOrgLimit = + existingLimits.orgActiveReleaseLimit !== null && + activeReleasesCount >= existingLimits.orgActiveReleaseLimit + const isOrgAtAboveOrgLimit = + existingLimits.orgActiveReleaseLimit !== null && + existingOrgActiveReleaseCount >= existingLimits.orgActiveReleaseLimit + + const shouldShowDialog = + isCurrentDatasetAtAboveDatasetLimit || + isCurrentDatasetAtAboveOrgLimit || + isOrgAtAboveOrgLimit + + if (shouldShowDialog) { + handleOpenDialog(existingOrgActiveReleaseCount) + if (throwError) { + throw new StudioReleaseLimitExceededError() + } + return } + + cb() }, [ activeReleases?.length, @@ -273,7 +282,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [mode, upsellDialogOpen, guardWithReleaseLimitUpsell, onReleaseLimitReached, telemetryLogs], ) - const interpolation = releaseLimit ? {releaseLimit} : undefined + const interpolation = existingCount ? {releaseLimit: existingCount} : undefined return ( diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 5244f74dc00..2591777e6c7 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -21,7 +21,10 @@ interface ReleaseLimits { orgActiveReleaseCount: number } -type UseOrgActiveReleaseCountReturn = () => Promise +type UseOrgActiveReleaseCountReturn = { + setOrgActiveReleaseCountManually: (count: number) => void + getOrgActiveReleaseCount: () => number | null +} const cacheTrigger$ = new BehaviorSubject(null) const CACHE_TTL_MS = 15000 diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx index d8f68315fbc..4d469b79ee2 100644 --- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx +++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx @@ -241,16 +241,23 @@ export function ReleasesOverview() { archivedReleases.length, ]) - const handleOnClickCreateRelease = useCallback( - () => guardWithReleaseLimitUpsell(() => setIsCreateReleaseDialogOpen(true)), - [guardWithReleaseLimitUpsell], - ) + const [isPendingGuard, setIsPendingGuard] = useState(false) + + const handleOnClickCreateRelease = useCallback(async () => { + setIsPendingGuard(true) + await guardWithReleaseLimitUpsell(() => { + setIsCreateReleaseDialogOpen(true) + }) + setIsPendingGuard(false) + }, [guardWithReleaseLimitUpsell]) const createReleaseButton = useMemo( () => ( ), - [hasCreatePermission, isCreateReleaseDialogOpen, mode, handleOnClickCreateRelease, tCore], + [ + isPendingGuard, + hasCreatePermission, + isCreateReleaseDialogOpen, + mode, + handleOnClickCreateRelease, + tCore, + ], ) const handleOnCreateRelease = useCallback( From deb03cf35e4f2e29d81070f77e279f565f1c7b14 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 00:37:17 +0000 Subject: [PATCH 13/38] refactor: updated useOrgActiveReleaseCount using a createStore --- .../upsell/ReleasesUpsellProvider.tsx | 6 +- .../store/useOrgActiveReleaseCount.ts | 143 +++++++++--------- 2 files changed, 78 insertions(+), 71 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 48fc22a01b2..56a83c9ae36 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -187,7 +187,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { ) const {getReleaseLimits, setLimitsManually} = useReleaseLimits() - const {getOrgActiveReleaseCount, setOrgActiveReleaseCountManually} = useOrgActiveReleaseCount() + const {getOrgActiveReleaseCount, setOrgActiveReleaseCount} = useOrgActiveReleaseCount() const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { @@ -211,7 +211,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { } if (existingOrgActiveReleaseCount === null) - setOrgActiveReleaseCountManually(limits.orgActiveReleaseCount) + setOrgActiveReleaseCount(limits.orgActiveReleaseCount) const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = limits existingLimits = { @@ -256,7 +256,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { getReleaseLimits, setLimitsManually, getOrgActiveReleaseCount, - setOrgActiveReleaseCountManually, + setOrgActiveReleaseCount, ], ) diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 2591777e6c7..d4231874f92 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -5,14 +5,15 @@ import { distinctUntilChanged, map, merge, + of, shareReplay, startWith, + Subject, switchMap, + timer, } from 'rxjs' -import {of} from 'rxjs/internal/observable/of' -import {timer} from 'rxjs/internal/observable/timer' -import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {type ResourceCache, useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {useActiveReleases} from './useActiveReleases' interface ReleaseLimits { @@ -21,69 +22,64 @@ interface ReleaseLimits { orgActiveReleaseCount: number } -type UseOrgActiveReleaseCountReturn = { - setOrgActiveReleaseCountManually: (count: number) => void - getOrgActiveReleaseCount: () => number | null -} - const cacheTrigger$ = new BehaviorSubject(null) const CACHE_TTL_MS = 15000 -export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { - const resourceCache = useResourceCache() - const {data: activeReleases} = useActiveReleases() - - const cache$ = useMemo( - () => - cacheTrigger$.pipe( - distinctUntilChanged(), - switchMap((activeReleasesCount) => { - if (activeReleasesCount === null) return of(null) - - const cachedState = resourceCache.get<{ - cachedValue: number - activeReleases: number - }>({ - namespace: 'OrgActiveReleasesCount', - dependencies: [activeReleases], - }) - - if (cachedState) { - const {cachedValue, activeReleases: cachedReleases} = cachedState - - if (cachedReleases === activeReleasesCount) { - console.log('Using cached value.') - - return merge( - of(cachedValue), - timer(CACHE_TTL_MS).pipe( - map(() => { - console.log('TTL expired, emitting null.') - resourceCache.set({ - namespace: 'OrgActiveReleasesCount', - dependencies: [activeReleases], - value: null, - }) - - cacheTrigger$.next(null) - return null - }), - ), - ) - } +export function createOrgActiveReleaseCountStore( + resourceCache: ResourceCache, + activeReleases: any, +) { + const dispatch$ = new Subject() + + // Observable that listens to cache updates & TTL expiration + const state$ = merge( + cacheTrigger$.pipe( + distinctUntilChanged(), + switchMap((activeReleasesCount) => { + if (activeReleasesCount === null) return of(null) + + const cachedState = resourceCache.get<{ + cachedValue: number + activeReleases: number + }>({ + namespace: 'OrgActiveReleasesCount', + dependencies: [activeReleases], + }) + + if (cachedState) { + const {cachedValue, activeReleases: cachedReleases} = cachedState + + if (cachedReleases === activeReleasesCount) { + console.log('Using cached value.') + + return merge( + of(cachedValue), + timer(CACHE_TTL_MS).pipe( + map(() => { + console.log('TTL expired, emitting null.') + resourceCache.set({ + namespace: 'OrgActiveReleasesCount', + dependencies: [activeReleases], + value: null, + }) + + cacheTrigger$.next(null) + return null + }), + ), + ) } + } - return of(null) - }), - startWith(null), - shareReplay({bufferSize: 1, refCount: true}), - ), - [activeReleases, resourceCache], - ) - - useObservable(cache$, null) + return of(null) + }), + startWith(null), + ), + dispatch$, + ).pipe(shareReplay({bufferSize: 1, refCount: true})) - const setOrgActiveReleaseCountManually = (count: number) => { + // Function to update cache manually + const setOrgActiveReleaseCount = (count: number) => { const activeReleasesCount = activeReleases?.length || 0 console.log('Storing orgActiveReleaseCount...') @@ -99,14 +95,25 @@ export const useOrgActiveReleaseCount = (): UseOrgActiveReleaseCountReturn => { cacheTrigger$.next(activeReleasesCount) } - const getOrgActiveReleaseCount = () => { - const cachedState = resourceCache.get<{cachedValue: number; activeReleases: number}>({ - namespace: 'OrgActiveReleasesCount', - dependencies: [activeReleases], - }) - - return cachedState?.cachedValue ?? null + return { + state$, + setOrgActiveReleaseCount, } +} + +export const useOrgActiveReleaseCount = () => { + const resourceCache = useResourceCache() + const {data: activeReleases} = useActiveReleases() - return {setOrgActiveReleaseCountManually, getOrgActiveReleaseCount} + const {state$, setOrgActiveReleaseCount} = useMemo( + () => createOrgActiveReleaseCountStore(resourceCache, activeReleases), + [resourceCache, activeReleases], + ) + + const cache = useObservable(state$, null) + + return { + getOrgActiveReleaseCount: () => cache, + setOrgActiveReleaseCount, + } } From 91e540535ebe022e9c1167dd8695c8c5062c7762 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 00:55:46 +0000 Subject: [PATCH 14/38] refactor: useReleaseLimits uses a createStore --- .../upsell/ReleasesUpsellProvider.tsx | 10 +- .../store/useOrgActiveReleaseCount.ts | 2 - .../core/releases/store/useReleaseLimits.ts | 103 ++++++++---------- 3 files changed, 52 insertions(+), 63 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 56a83c9ae36..2d44bf6eece 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -186,7 +186,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [telemetry], ) - const {getReleaseLimits, setLimitsManually} = useReleaseLimits() + const {getReleaseLimits, setReleaseLimits} = useReleaseLimits() const {getOrgActiveReleaseCount, setOrgActiveReleaseCount} = useOrgActiveReleaseCount() const guardWithReleaseLimitUpsell = useCallback( @@ -204,7 +204,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { if (existingLimits === null) { console.log('setting release limits', {limits}) - setLimitsManually({ + setReleaseLimits({ datasetReleaseLimit: limits.datasetReleaseLimit, orgActiveReleaseLimit: limits.orgActiveReleaseLimit, }) @@ -251,12 +251,12 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { cb() }, [ - activeReleases?.length, - handleOpenDialog, getReleaseLimits, - setLimitsManually, getOrgActiveReleaseCount, + activeReleases?.length, setOrgActiveReleaseCount, + setReleaseLimits, + handleOpenDialog, ], ) diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index d4231874f92..e480d8ffd74 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -31,7 +31,6 @@ export function createOrgActiveReleaseCountStore( ) { const dispatch$ = new Subject() - // Observable that listens to cache updates & TTL expiration const state$ = merge( cacheTrigger$.pipe( distinctUntilChanged(), @@ -78,7 +77,6 @@ export function createOrgActiveReleaseCountStore( dispatch$, ).pipe(shareReplay({bufferSize: 1, refCount: true})) - // Function to update cache manually const setOrgActiveReleaseCount = (count: number) => { const activeReleasesCount = activeReleases?.length || 0 diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 5e91ed78e9e..ce88ec317dc 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,22 +1,9 @@ import {useMemo} from 'react' import {useObservable} from 'react-rx' -import {BehaviorSubject, defer, delay, firstValueFrom, map, of, shareReplay, tap} from 'rxjs' - -import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' - -export const fetchReleasesLimits = () => - of({ - orgActiveReleaseCount: 10, - orgActiveReleaseLimit: 20, - datasetReleaseLimit: 6, - - // orgActiveReleaseCount: 6, - // orgActiveReleaseLimit: 6, - // datasetReleaseLimit: 10, - }).pipe( - tap(() => console.log('SEE THIS ONLY ONCE fetchReleasesLimits')), - delay(3000), - ) +import {BehaviorSubject, firstValueFrom, shareReplay} from 'rxjs' + +import {type ResourceCache, useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' interface ReleaseLimits { datasetReleaseLimit: number @@ -25,47 +12,38 @@ interface ReleaseLimits { const releaseLimitsSubject = new BehaviorSubject(null) -export const useReleaseLimits = () => { - const resourceCache = useResourceCache() +export function createReleaseLimitsStore(resourceCache: ResourceCache) { + const state$ = releaseLimitsSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) - const releaseLimits$ = useMemo(() => { - return defer(() => { - const cachedState = resourceCache.get({ + const fetchReleaseLimits = async () => { + const cachedState = + releaseLimitsSubject.getValue() ?? + resourceCache.get({ namespace: 'ReleaseLimits', dependencies: [], }) - if (cachedState) { - console.log('Using cached ReleaseLimits') - return of(cachedState) - } - - console.log('Fetching ReleaseLimits...') - return fetchReleasesLimits().pipe( - map(({datasetReleaseLimit, orgActiveReleaseLimit}) => { - const limits: ReleaseLimits = {datasetReleaseLimit, orgActiveReleaseLimit} - - resourceCache.set({ - namespace: 'ReleaseLimits', - dependencies: [], - value: limits, - }) - - releaseLimitsSubject.next(limits) + if (cachedState) { + console.log('Returning already cached ReleaseLimits') + releaseLimitsSubject.next(cachedState) + return cachedState + } - return limits - }), - ) - }).pipe(shareReplay({bufferSize: 1, refCount: true})) - }, [resourceCache]) + console.log('Fetching ReleaseLimits...') + const limits = await firstValueFrom(fetchReleasesLimits()) - const releasesLimit = useObservable(releaseLimitsSubject, null) + resourceCache.set({ + namespace: 'ReleaseLimits', + dependencies: [], + value: limits, + }) + releaseLimitsSubject.next(limits) - const fetchReleaseLimits = async () => releasesLimit || firstValueFrom(releaseLimits$) + return limits + } - const setLimitsManually = (limits: ReleaseLimits) => { + const setReleaseLimits = (limits: ReleaseLimits) => { console.log('Storing ReleaseLimits...') - resourceCache.set({ namespace: 'ReleaseLimits', dependencies: [], @@ -75,13 +53,26 @@ export const useReleaseLimits = () => { releaseLimitsSubject.next(limits) } - const getReleaseLimits = () => - releasesLimit || - resourceCache.get({ - namespace: 'ReleaseLimits', - dependencies: [], - }) || - null + return { + state$, + setReleaseLimits, + fetchReleaseLimits, + } +} - return {fetchReleaseLimits, setLimitsManually, getReleaseLimits} +export const useReleaseLimits = () => { + const resourceCache = useResourceCache() + + const {state$, setReleaseLimits, fetchReleaseLimits} = useMemo( + () => createReleaseLimitsStore(resourceCache), + [resourceCache], + ) + + const cache = useObservable(state$, null) + + return { + getReleaseLimits: () => cache, + setReleaseLimits, + fetchReleaseLimits, + } } From 6af633c0eeba446f8832f38f1ba37b669b40dd32 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 03:35:18 +0000 Subject: [PATCH 15/38] refactor: using the count of org active releases from the store directly to interpolate on upsell --- .../core/releases/contexts/upsell/ReleasesUpsellProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 2d44bf6eece..dc8b010d3c8 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -282,7 +282,9 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [mode, upsellDialogOpen, guardWithReleaseLimitUpsell, onReleaseLimitReached, telemetryLogs], ) - const interpolation = existingCount ? {releaseLimit: existingCount} : undefined + const interpolation = getOrgActiveReleaseCount() + ? {releaseLimit: getOrgActiveReleaseCount()} + : undefined return ( From 9694ffccda612c2cbd640f74ac5363611f1e5721 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 04:21:57 +0000 Subject: [PATCH 16/38] feat: storing fetching state and error state for useReleaseLimits --- .../core/releases/hooks/useIsReleasesPlus.ts | 28 +++++++----- .../core/releases/store/useReleaseLimits.ts | 45 +++++++++++++++---- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index dcc8c38c4a4..7afd13dd452 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -5,21 +5,25 @@ import {useReleaseLimits} from '../store/useReleaseLimits' const RELEASES_PLUS_LIMIT = 2 export const useIsReleasesPlus = () => { - const {fetchReleaseLimits} = useReleaseLimits() + const {fetchReleaseLimits, isFetching, isError} = useReleaseLimits() - const [isReleasesPlus, setIsReleasesPlus] = useState(undefined) - const [hasFetchedLimits, setHasFetchedLimits] = useState(false) + const [isReleasesPlus, setIsReleasesPlus] = useState(null) useEffect(() => { - if (isReleasesPlus !== undefined || hasFetchedLimits) return - - setHasFetchedLimits(true) - fetchReleaseLimits() - .then((limits) => - setIsReleasesPlus((limits.orgActiveReleaseLimit || 0) > RELEASES_PLUS_LIMIT), - ) - .catch(() => setIsReleasesPlus(false)) - }, [fetchReleaseLimits, hasFetchedLimits, isReleasesPlus]) + if (isReleasesPlus !== null || isFetching) return + + fetchReleaseLimits().then((limits) => { + if (limits?.orgActiveReleaseLimit) { + const {orgActiveReleaseLimit} = limits + + setIsReleasesPlus(orgActiveReleaseLimit > RELEASES_PLUS_LIMIT) + } + }) + }, [fetchReleaseLimits, isFetching, isReleasesPlus]) + + useEffect(() => { + if (isError) setIsReleasesPlus(false) + }, [isError]) return isReleasesPlus } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index ce88ec317dc..e9927161784 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -11,9 +11,13 @@ interface ReleaseLimits { } const releaseLimitsSubject = new BehaviorSubject(null) +const isFetchingSubject = new BehaviorSubject(false) +const isErrorSubject = new BehaviorSubject(false) export function createReleaseLimitsStore(resourceCache: ResourceCache) { const state$ = releaseLimitsSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) + const isFetching$ = isFetchingSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) + const isError$ = isErrorSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) const fetchReleaseLimits = async () => { const cachedState = @@ -26,20 +30,36 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { if (cachedState) { console.log('Returning already cached ReleaseLimits') releaseLimitsSubject.next(cachedState) + isErrorSubject.next(false) return cachedState } + if (isFetchingSubject.value) { + console.log('Fetch already in progress, skipping...') + return null + } + console.log('Fetching ReleaseLimits...') - const limits = await firstValueFrom(fetchReleasesLimits()) + isFetchingSubject.next(true) + isErrorSubject.next(false) - resourceCache.set({ - namespace: 'ReleaseLimits', - dependencies: [], - value: limits, - }) - releaseLimitsSubject.next(limits) + try { + const limits = await firstValueFrom(fetchReleasesLimits()) - return limits + resourceCache.set({ + namespace: 'ReleaseLimits', + dependencies: [], + value: limits, + }) + releaseLimitsSubject.next(limits) + return limits + } catch (error) { + console.error('Error fetching ReleaseLimits:', error) + isErrorSubject.next(true) + return null + } finally { + isFetchingSubject.next(false) + } } const setReleaseLimits = (limits: ReleaseLimits) => { @@ -51,10 +71,13 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { }) releaseLimitsSubject.next(limits) + isErrorSubject.next(false) } return { state$, + isFetching$, + isError$, setReleaseLimits, fetchReleaseLimits, } @@ -63,15 +86,19 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { export const useReleaseLimits = () => { const resourceCache = useResourceCache() - const {state$, setReleaseLimits, fetchReleaseLimits} = useMemo( + const {state$, isFetching$, isError$, setReleaseLimits, fetchReleaseLimits} = useMemo( () => createReleaseLimitsStore(resourceCache), [resourceCache], ) const cache = useObservable(state$, null) + const isFetching = useObservable(isFetching$, false) + const isError = useObservable(isError$, false) return { getReleaseLimits: () => cache, + isFetching, + isError, setReleaseLimits, fetchReleaseLimits, } From 78f8f37720d12dd32505e1b186b1a810535b4fff Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 04:55:41 +0000 Subject: [PATCH 17/38] refactor: updating useReleaseLimits to use single subject for fetch state --- .../upsell/ReleasesUpsellProvider.tsx | 6 +++ .../core/releases/store/useReleaseLimits.ts | 49 +++++++++++-------- .../src/core/studio/upsell/UpsellDialog.tsx | 12 ++++- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index dc8b010d3c8..bea2a8090c8 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -57,6 +57,12 @@ export const fetchReleasesLimits = () => delay(3000), ) +// export const fetchReleasesLimits = () => +// throwError(() => new Error('Simulated API failure')).pipe( +// tap(() => console.log('fetchReleasesLimits - Simulating failure')), +// delay(3000), +// ) + /** * @beta * @hidden diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index e9927161784..5dcd6ce72bf 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,6 +1,6 @@ import {useMemo} from 'react' import {useObservable} from 'react-rx' -import {BehaviorSubject, firstValueFrom, shareReplay} from 'rxjs' +import {BehaviorSubject, firstValueFrom, scan, shareReplay} from 'rxjs' import {type ResourceCache, useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' @@ -10,14 +10,27 @@ interface ReleaseLimits { orgActiveReleaseLimit: number | null } +interface FetchState { + isFetching: boolean + isError: boolean +} + const releaseLimitsSubject = new BehaviorSubject(null) -const isFetchingSubject = new BehaviorSubject(false) -const isErrorSubject = new BehaviorSubject(false) +const fetchStateSubject = new BehaviorSubject>({}) export function createReleaseLimitsStore(resourceCache: ResourceCache) { const state$ = releaseLimitsSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) - const isFetching$ = isFetchingSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) - const isError$ = isErrorSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) + + const fetchState$ = fetchStateSubject.pipe( + scan, FetchState>( + (state, patch) => ({ + isFetching: patch.isFetching ?? state.isFetching, + isError: patch.isError ?? state.isError, + }), + {isFetching: false, isError: false}, + ), + shareReplay({bufferSize: 1, refCount: true}), + ) const fetchReleaseLimits = async () => { const cachedState = @@ -30,18 +43,17 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { if (cachedState) { console.log('Returning already cached ReleaseLimits') releaseLimitsSubject.next(cachedState) - isErrorSubject.next(false) + fetchStateSubject.next({isError: false}) return cachedState } - if (isFetchingSubject.value) { + if (fetchStateSubject.getValue()?.isFetching) { console.log('Fetch already in progress, skipping...') return null } console.log('Fetching ReleaseLimits...') - isFetchingSubject.next(true) - isErrorSubject.next(false) + fetchStateSubject.next({isFetching: true, isError: false}) try { const limits = await firstValueFrom(fetchReleasesLimits()) @@ -52,13 +64,12 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { value: limits, }) releaseLimitsSubject.next(limits) + fetchStateSubject.next({isFetching: false, isError: false}) return limits } catch (error) { console.error('Error fetching ReleaseLimits:', error) - isErrorSubject.next(true) + fetchStateSubject.next({isFetching: false, isError: true}) return null - } finally { - isFetchingSubject.next(false) } } @@ -71,13 +82,12 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { }) releaseLimitsSubject.next(limits) - isErrorSubject.next(false) + fetchStateSubject.next({isError: false}) } return { state$, - isFetching$, - isError$, + fetchState$, setReleaseLimits, fetchReleaseLimits, } @@ -86,19 +96,18 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { export const useReleaseLimits = () => { const resourceCache = useResourceCache() - const {state$, isFetching$, isError$, setReleaseLimits, fetchReleaseLimits} = useMemo( + const {state$, fetchState$, setReleaseLimits, fetchReleaseLimits} = useMemo( () => createReleaseLimitsStore(resourceCache), [resourceCache], ) const cache = useObservable(state$, null) - const isFetching = useObservable(isFetching$, false) - const isError = useObservable(isError$, false) + const fetchState = useObservable(fetchState$, {isFetching: false, isError: false}) return { getReleaseLimits: () => cache, - isFetching, - isError, + isFetching: fetchState.isFetching, + isError: fetchState.isError, setReleaseLimits, fetchReleaseLimits, } diff --git a/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx b/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx index 5375104418c..b081e9567c2 100644 --- a/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx +++ b/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx @@ -3,6 +3,7 @@ import {CloseIcon, LaunchIcon} from '@sanity/icons' import {Box, Stack} from '@sanity/ui' // eslint-disable-next-line camelcase import {getTheme_v2} from '@sanity/ui/theme' +import {useEffect, useState} from 'react' import {styled} from 'styled-components' import {Button, Dialog} from '../../../ui-components' @@ -47,6 +48,15 @@ interface UpsellDialogProps { export function UpsellDialog(props: UpsellDialogProps) { const {data, onClose, onPrimaryClick, onSecondaryClick, interpolation} = props + const [stableInterpolation, setStableInterpolation] = useState(interpolation) + + useEffect(() => { + // prevent interpolation from resetting on re-render + // so that it keeps the value it was first rendered with + if (interpolation !== undefined) { + setStableInterpolation(interpolation) + } + }, [interpolation]) return ( From 9409d9354037d8ae0235cfaed4db28d53dc2f157 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 12:39:55 +0000 Subject: [PATCH 18/38] refactor: renaming --- .../upsell/ReleasesUpsellProvider.tsx | 47 +++++++------- .../store/useOrgActiveReleaseCount.ts | 43 ++++++------- .../core/releases/store/useReleaseLimits.ts | 62 ++++++++++--------- 3 files changed, 75 insertions(+), 77 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index bea2a8090c8..791dcd2a2be 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -176,21 +176,15 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { } }, [client, projectId]) - const [existingCount, setExistingCount] = useState(0) - - const handleOpenDialog = useCallback( - (_existingCount: number = 0) => { - setExistingCount(_existingCount) - setUpsellDialogOpen(true) - - telemetry.log(UpsellDialogViewed, { - feature: FEATURE, - type: 'modal', - source: 'navbar', - }) - }, - [telemetry], - ) + const handleOpenDialog = useCallback(() => { + setUpsellDialogOpen(true) + + telemetry.log(UpsellDialogViewed, { + feature: FEATURE, + type: 'modal', + source: 'navbar', + }) + }, [telemetry]) const {getReleaseLimits, setReleaseLimits} = useReleaseLimits() const {getOrgActiveReleaseCount, setOrgActiveReleaseCount} = useOrgActiveReleaseCount() @@ -206,20 +200,24 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { console.log('Fetching new data since cache is missing or expired...') try { - const limits = await firstValueFrom(fetchReleasesLimits()) + const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = + await firstValueFrom(fetchReleasesLimits()) if (existingLimits === null) { - console.log('setting release limits', {limits}) + console.log('setting release limits', { + datasetReleaseLimit, + orgActiveReleaseLimit, + orgActiveReleaseCount, + }) setReleaseLimits({ - datasetReleaseLimit: limits.datasetReleaseLimit, - orgActiveReleaseLimit: limits.orgActiveReleaseLimit, + datasetReleaseLimit, + orgActiveReleaseLimit, }) } if (existingOrgActiveReleaseCount === null) - setOrgActiveReleaseCount(limits.orgActiveReleaseCount) + setOrgActiveReleaseCount(orgActiveReleaseCount) - const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = limits existingLimits = { datasetReleaseLimit, orgActiveReleaseLimit, @@ -247,7 +245,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { isOrgAtAboveOrgLimit if (shouldShowDialog) { - handleOpenDialog(existingOrgActiveReleaseCount) + handleOpenDialog() if (throwError) { throw new StudioReleaseLimitExceededError() } @@ -288,8 +286,9 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [mode, upsellDialogOpen, guardWithReleaseLimitUpsell, onReleaseLimitReached, telemetryLogs], ) - const interpolation = getOrgActiveReleaseCount() - ? {releaseLimit: getOrgActiveReleaseCount()} + const currentReleaseLimitCount = getOrgActiveReleaseCount() + const interpolation = currentReleaseLimitCount + ? {releaseLimit: currentReleaseLimitCount} : undefined return ( diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index e480d8ffd74..fa9ce1b545b 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -16,53 +16,48 @@ import { import {type ResourceCache, useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {useActiveReleases} from './useActiveReleases' -interface ReleaseLimits { - datasetReleaseLimit: number - orgActiveReleaseLimit: number | null - orgActiveReleaseCount: number -} - -const cacheTrigger$ = new BehaviorSubject(null) -const CACHE_TTL_MS = 15000 +const STATE_TTL_MS = 15000 +const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'OrgActiveReleasesCount' -export function createOrgActiveReleaseCountStore( +function createOrgActiveReleaseCountStore( resourceCache: ResourceCache, - activeReleases: any, + activeReleases: ReturnType['data'], ) { const dispatch$ = new Subject() + const stateTrigger$ = new BehaviorSubject(null) const state$ = merge( - cacheTrigger$.pipe( + stateTrigger$.pipe( distinctUntilChanged(), switchMap((activeReleasesCount) => { if (activeReleasesCount === null) return of(null) const cachedState = resourceCache.get<{ - cachedValue: number + orgActiveReleaseCount: number activeReleases: number }>({ - namespace: 'OrgActiveReleasesCount', + namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, dependencies: [activeReleases], }) if (cachedState) { - const {cachedValue, activeReleases: cachedReleases} = cachedState + const {orgActiveReleaseCount, activeReleases: cachedReleases} = cachedState if (cachedReleases === activeReleasesCount) { console.log('Using cached value.') return merge( - of(cachedValue), - timer(CACHE_TTL_MS).pipe( + of(orgActiveReleaseCount), + timer(STATE_TTL_MS).pipe( map(() => { console.log('TTL expired, emitting null.') resourceCache.set({ - namespace: 'OrgActiveReleasesCount', + namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, dependencies: [activeReleases], value: null, }) - cacheTrigger$.next(null) + stateTrigger$.next(null) return null }), ), @@ -77,20 +72,20 @@ export function createOrgActiveReleaseCountStore( dispatch$, ).pipe(shareReplay({bufferSize: 1, refCount: true})) - const setOrgActiveReleaseCount = (count: number) => { + const setOrgActiveReleaseCount = (orgActiveReleaseCount: number) => { const activeReleasesCount = activeReleases?.length || 0 console.log('Storing orgActiveReleaseCount...') resourceCache.set({ - namespace: 'OrgActiveReleasesCount', + namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, dependencies: [activeReleases], value: { - cachedValue: count, + orgActiveReleaseCount, activeReleases: activeReleasesCount, }, }) - cacheTrigger$.next(activeReleasesCount) + stateTrigger$.next(activeReleasesCount) } return { @@ -108,10 +103,10 @@ export const useOrgActiveReleaseCount = () => { [resourceCache, activeReleases], ) - const cache = useObservable(state$, null) + const state = useObservable(state$, null) return { - getOrgActiveReleaseCount: () => cache, + getOrgActiveReleaseCount: () => state, setOrgActiveReleaseCount, } } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 5dcd6ce72bf..83c136d06ed 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -15,13 +15,15 @@ interface FetchState { isError: boolean } -const releaseLimitsSubject = new BehaviorSubject(null) -const fetchStateSubject = new BehaviorSubject>({}) +const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' -export function createReleaseLimitsStore(resourceCache: ResourceCache) { - const state$ = releaseLimitsSubject.pipe(shareReplay({bufferSize: 1, refCount: true})) +const releaseLimits$ = new BehaviorSubject(null) +const fetchState$ = new BehaviorSubject>({}) - const fetchState$ = fetchStateSubject.pipe( +function createReleaseLimitsStore(resourceCache: ResourceCache) { + const state$ = releaseLimits$.pipe(shareReplay({bufferSize: 1, refCount: true})) + + const fetchState = fetchState$.pipe( scan, FetchState>( (state, patch) => ({ isFetching: patch.isFetching ?? state.isFetching, @@ -33,42 +35,42 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { ) const fetchReleaseLimits = async () => { - const cachedState = - releaseLimitsSubject.getValue() ?? + const latestState = + releaseLimits$.getValue() ?? resourceCache.get({ - namespace: 'ReleaseLimits', + namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, dependencies: [], }) - if (cachedState) { + if (latestState) { console.log('Returning already cached ReleaseLimits') - releaseLimitsSubject.next(cachedState) - fetchStateSubject.next({isError: false}) - return cachedState + releaseLimits$.next(latestState) + fetchState$.next({isError: false, isFetching: false}) + return latestState } - if (fetchStateSubject.getValue()?.isFetching) { + if (fetchState$.getValue()?.isFetching) { console.log('Fetch already in progress, skipping...') return null } console.log('Fetching ReleaseLimits...') - fetchStateSubject.next({isFetching: true, isError: false}) + fetchState$.next({isFetching: true, isError: false}) try { const limits = await firstValueFrom(fetchReleasesLimits()) resourceCache.set({ - namespace: 'ReleaseLimits', + namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, dependencies: [], value: limits, }) - releaseLimitsSubject.next(limits) - fetchStateSubject.next({isFetching: false, isError: false}) + releaseLimits$.next(limits) + fetchState$.next({isFetching: false, isError: false}) return limits } catch (error) { console.error('Error fetching ReleaseLimits:', error) - fetchStateSubject.next({isFetching: false, isError: true}) + fetchState$.next({isFetching: false, isError: true}) return null } } @@ -76,18 +78,18 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { const setReleaseLimits = (limits: ReleaseLimits) => { console.log('Storing ReleaseLimits...') resourceCache.set({ - namespace: 'ReleaseLimits', + namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, dependencies: [], value: limits, }) - releaseLimitsSubject.next(limits) - fetchStateSubject.next({isError: false}) + releaseLimits$.next(limits) + fetchState$.next({isError: false, isFetching: false}) } return { state$, - fetchState$, + fetchState$: fetchState, setReleaseLimits, fetchReleaseLimits, } @@ -96,16 +98,18 @@ export function createReleaseLimitsStore(resourceCache: ResourceCache) { export const useReleaseLimits = () => { const resourceCache = useResourceCache() - const {state$, fetchState$, setReleaseLimits, fetchReleaseLimits} = useMemo( - () => createReleaseLimitsStore(resourceCache), - [resourceCache], - ) + const { + state$, + fetchState$: _fetchState$, + setReleaseLimits, + fetchReleaseLimits, + } = useMemo(() => createReleaseLimitsStore(resourceCache), [resourceCache]) - const cache = useObservable(state$, null) - const fetchState = useObservable(fetchState$, {isFetching: false, isError: false}) + const state = useObservable(state$, null) + const fetchState = useObservable(_fetchState$, {isFetching: false, isError: false}) return { - getReleaseLimits: () => cache, + getReleaseLimits: () => state, isFetching: fetchState.isFetching, isError: fetchState.isError, setReleaseLimits, From f8f7453303a4a955b8648a9906809665d559dfa0 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 20:50:37 +0000 Subject: [PATCH 19/38] refactor: to using more reactivity --- .../upsell/ReleasesUpsellProvider.tsx | 152 ++++++++--------- .../core/releases/contexts/upsell/types.ts | 2 +- .../contexts/upsell/useReleasesUpsell.ts | 2 +- .../core/releases/hooks/useIsReleasesPlus.ts | 25 +-- .../core/releases/store/createReleaseStore.ts | 7 + .../sanity/src/core/releases/store/types.ts | 1 + .../store/useOrgActiveReleaseCount.ts | 159 +++++++----------- .../core/releases/store/useReleaseLimits.ts | 122 +++----------- 8 files changed, 172 insertions(+), 298 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 791dcd2a2be..0ceacce7b22 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,8 +1,9 @@ +/* eslint-disable no-console */ import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' import {firstValueFrom, of} from 'rxjs' -import {delay, tap} from 'rxjs/operators' +import {delay, shareReplay, tap} from 'rxjs/operators' import {ReleasesUpsellContext} from 'sanity/_singletons' import {useClient, useFeatureEnabled, useProjectId} from '../../../hooks' @@ -20,12 +21,6 @@ import {useOrgActiveReleaseCount} from '../../store/useOrgActiveReleaseCount' import {useReleaseLimits} from '../../store/useReleaseLimits' import {type ReleasesUpsellContextValue} from './types' -type ReleaseLimits = { - datasetReleaseLimit: number - orgActiveReleaseCount: number - orgActiveReleaseLimit: number -} - class StudioReleaseLimitExceededError extends Error { details: {type: 'releaseLimitExceededError'} @@ -45,7 +40,7 @@ const API_VERSION = '2024-04-19' export const fetchReleasesLimits = () => of({ - orgActiveReleaseCount: 10, + orgActiveReleaseCount: 6, orgActiveReleaseLimit: 20, datasetReleaseLimit: 6, @@ -53,6 +48,7 @@ export const fetchReleasesLimits = () => // orgActiveReleaseLimit: 6, // datasetReleaseLimit: 10, }).pipe( + shareReplay(1), tap(() => console.log('fetchReleasesLimits')), delay(3000), ) @@ -176,75 +172,30 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { } }, [client, projectId]) - const handleOpenDialog = useCallback(() => { - setUpsellDialogOpen(true) - - telemetry.log(UpsellDialogViewed, { - feature: FEATURE, - type: 'modal', - source: 'navbar', - }) - }, [telemetry]) - - const {getReleaseLimits, setReleaseLimits} = useReleaseLimits() - const {getOrgActiveReleaseCount, setOrgActiveReleaseCount} = useOrgActiveReleaseCount() - - const guardWithReleaseLimitUpsell = useCallback( - async (cb: () => void, throwError: boolean = false) => { - console.log('Guard called, checking caches...') - - let existingLimits = getReleaseLimits() - let existingOrgActiveReleaseCount = getOrgActiveReleaseCount() - - if (existingLimits === null || existingOrgActiveReleaseCount === null) { - console.log('Fetching new data since cache is missing or expired...') - - try { - const {datasetReleaseLimit, orgActiveReleaseLimit, orgActiveReleaseCount} = - await firstValueFrom(fetchReleasesLimits()) - - if (existingLimits === null) { - console.log('setting release limits', { - datasetReleaseLimit, - orgActiveReleaseLimit, - orgActiveReleaseCount, - }) - setReleaseLimits({ - datasetReleaseLimit, - orgActiveReleaseLimit, - }) - } + const [releaseCount, setReleaseCount] = useState(null) - if (existingOrgActiveReleaseCount === null) - setOrgActiveReleaseCount(orgActiveReleaseCount) - - existingLimits = { - datasetReleaseLimit, - orgActiveReleaseLimit, - } - existingOrgActiveReleaseCount = orgActiveReleaseCount - } catch (e) { - console.error('Error fetching release limits:', e) - } + const handleOpenDialog = useCallback( + (orgActiveReleaseCount?: number) => { + setUpsellDialogOpen(true) + if (orgActiveReleaseCount !== undefined) { + setReleaseCount(orgActiveReleaseCount) } - const activeReleasesCount = activeReleases?.length || 0 - - const isCurrentDatasetAtAboveDatasetLimit = - activeReleasesCount >= existingLimits.datasetReleaseLimit - const isCurrentDatasetAtAboveOrgLimit = - existingLimits.orgActiveReleaseLimit !== null && - activeReleasesCount >= existingLimits.orgActiveReleaseLimit - const isOrgAtAboveOrgLimit = - existingLimits.orgActiveReleaseLimit !== null && - existingOrgActiveReleaseCount >= existingLimits.orgActiveReleaseLimit + telemetry.log(UpsellDialogViewed, { + feature: FEATURE, + type: 'modal', + source: 'navbar', + }) + }, + [telemetry], + ) - const shouldShowDialog = - isCurrentDatasetAtAboveDatasetLimit || - isCurrentDatasetAtAboveOrgLimit || - isOrgAtAboveOrgLimit + const {releaseLimits$} = useReleaseLimits() + const {orgActiveReleaseCount$} = useOrgActiveReleaseCount() - if (shouldShowDialog) { + const guardWithReleaseLimitUpsell = useCallback( + async (cb: () => void, throwError: boolean = false) => { + if (mode === 'upsell') { handleOpenDialog() if (throwError) { throw new StudioReleaseLimitExceededError() @@ -252,16 +203,50 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { return } - cb() + try { + console.log('Guard called, checking caches...') + // if either fails then catch the error + const [orgActiveReleaseCount, releaseLimits] = await Promise.all([ + firstValueFrom(orgActiveReleaseCount$), + firstValueFrom(releaseLimits$), + ]) + + const {orgActiveReleaseLimit, datasetReleaseLimit} = releaseLimits + + // orgActiveReleaseCount might be missing due to internal server error + // allow pass through guard in that case + if (orgActiveReleaseCount) { + const activeReleasesCount = activeReleases?.length || 0 + + const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit + const isCurrentDatasetAtAboveOrgLimit = + orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit + const isOrgAtAboveOrgLimit = + orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit + + const shouldShowDialog = + isCurrentDatasetAtAboveDatasetLimit || + isCurrentDatasetAtAboveOrgLimit || + isOrgAtAboveOrgLimit + + if (shouldShowDialog) { + handleOpenDialog(orgActiveReleaseCount) + if (throwError) { + throw new StudioReleaseLimitExceededError() + } + return + } + } + + cb() + } catch (e) { + console.error('Error fetching release limits for upsell:', e) + + // silently fail and allow pass through guard + cb() + } }, - [ - getReleaseLimits, - getOrgActiveReleaseCount, - activeReleases?.length, - setOrgActiveReleaseCount, - setReleaseLimits, - handleOpenDialog, - ], + [mode, handleOpenDialog, orgActiveReleaseCount$, releaseLimits$, activeReleases?.length], ) const onReleaseLimitReached = useCallback( @@ -286,10 +271,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [mode, upsellDialogOpen, guardWithReleaseLimitUpsell, onReleaseLimitReached, telemetryLogs], ) - const currentReleaseLimitCount = getOrgActiveReleaseCount() - const interpolation = currentReleaseLimitCount - ? {releaseLimit: currentReleaseLimitCount} - : undefined + const interpolation = releaseCount ? {releaseLimit: releaseCount} : undefined return ( diff --git a/packages/sanity/src/core/releases/contexts/upsell/types.ts b/packages/sanity/src/core/releases/contexts/upsell/types.ts index c8938335f8d..945c62b2297 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/types.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/types.ts @@ -8,7 +8,7 @@ export interface ReleasesUpsellContextValue { */ mode: 'upsell' | 'default' | 'disabled' upsellDialogOpen: boolean - guardWithReleaseLimitUpsell: (callback: () => void, throwError?: boolean) => false | void + guardWithReleaseLimitUpsell: (callback: () => void, throwError?: boolean) => Promise onReleaseLimitReached: (limit: number, suppressDialogOpening: boolean) => void telemetryLogs: { dialogSecondaryClicked: () => void diff --git a/packages/sanity/src/core/releases/contexts/upsell/useReleasesUpsell.ts b/packages/sanity/src/core/releases/contexts/upsell/useReleasesUpsell.ts index e8b065e568f..6236103cfd2 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/useReleasesUpsell.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/useReleasesUpsell.ts @@ -19,7 +19,7 @@ const FALLBACK_CONTEXT_VALUE = { upsellDialogOpen: false, mode: 'default' as const, onReleaseLimitReached: () => null, - guardWithReleaseLimitUpsell: () => undefined, + guardWithReleaseLimitUpsell: async () => undefined, telemetryLogs: { dialogSecondaryClicked: () => null, dialogPrimaryClicked: () => null, diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index 7afd13dd452..2b7147b5d6d 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -1,29 +1,16 @@ -import {useEffect, useState} from 'react' +import {useObservable} from 'react-rx' import {useReleaseLimits} from '../store/useReleaseLimits' const RELEASES_PLUS_LIMIT = 2 export const useIsReleasesPlus = () => { - const {fetchReleaseLimits, isFetching, isError} = useReleaseLimits() + const {releaseLimits$} = useReleaseLimits() - const [isReleasesPlus, setIsReleasesPlus] = useState(null) + const releaseLimit = useObservable(releaseLimits$, null) - useEffect(() => { - if (isReleasesPlus !== null || isFetching) return + const {orgActiveReleaseLimit} = releaseLimit || {} - fetchReleaseLimits().then((limits) => { - if (limits?.orgActiveReleaseLimit) { - const {orgActiveReleaseLimit} = limits - - setIsReleasesPlus(orgActiveReleaseLimit > RELEASES_PLUS_LIMIT) - } - }) - }, [fetchReleaseLimits, isFetching, isReleasesPlus]) - - useEffect(() => { - if (isError) setIsReleasesPlus(false) - }, [isError]) - - return isReleasesPlus + // presume not releases+ if empty data + return orgActiveReleaseLimit && orgActiveReleaseLimit >= RELEASES_PLUS_LIMIT } diff --git a/packages/sanity/src/core/releases/store/createReleaseStore.ts b/packages/sanity/src/core/releases/store/createReleaseStore.ts index 5c69a280958..c78a73365b6 100644 --- a/packages/sanity/src/core/releases/store/createReleaseStore.ts +++ b/packages/sanity/src/core/releases/store/createReleaseStore.ts @@ -22,6 +22,7 @@ import {distinctUntilChanged, map, startWith} from 'rxjs/operators' import {type DocumentPreviewStore} from '../../preview' import {listenQuery} from '../../store/_legacy' +import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' import {RELEASE_DOCUMENT_TYPE, RELEASE_DOCUMENTS_PATH} from './constants' import {createReleaseMetadataAggregator} from './createReleaseMetadataAggregator' import {releasesReducer, type ReleasesReducerAction, type ReleasesReducerState} from './reducer' @@ -147,10 +148,12 @@ export function createReleaseStore(context: { ) const errorCount$ = state$.pipe(releaseStoreErrorCount(), shareReplay(1)) + const releaseLimits$ = releasesStoreLimits().pipe(shareReplay(1)) const getMetadataStateForSlugs$ = createReleaseMetadataAggregator(client) return { + releaseLimits$, state$, errorCount$, getMetadataStateForSlugs$, @@ -173,3 +176,7 @@ export function releaseStoreErrorCount(): OperatorFunction + releaseLimits$: Observable<{orgActiveReleaseLimit: number}> /** * Counts all loaded release documents that are in an active state and have an error recorded. * This is determined by the presence of the `error` field in the release document. diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index fa9ce1b545b..24cd5072cb0 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,96 +1,55 @@ +/* eslint-disable no-console */ import {useMemo} from 'react' -import {useObservable} from 'react-rx' -import { - BehaviorSubject, - distinctUntilChanged, - map, - merge, - of, - shareReplay, - startWith, - Subject, - switchMap, - timer, -} from 'rxjs' - -import {type ResourceCache, useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {BehaviorSubject, map, type Observable, of, switchMap, tap, timer} from 'rxjs' + +import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' import {useActiveReleases} from './useActiveReleases' -const STATE_TTL_MS = 15000 -const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'OrgActiveReleasesCount' - -function createOrgActiveReleaseCountStore( - resourceCache: ResourceCache, - activeReleases: ReturnType['data'], -) { - const dispatch$ = new Subject() - const stateTrigger$ = new BehaviorSubject(null) - - const state$ = merge( - stateTrigger$.pipe( - distinctUntilChanged(), - switchMap((activeReleasesCount) => { - if (activeReleasesCount === null) return of(null) - - const cachedState = resourceCache.get<{ - orgActiveReleaseCount: number - activeReleases: number - }>({ - namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, - dependencies: [activeReleases], - }) - - if (cachedState) { - const {orgActiveReleaseCount, activeReleases: cachedReleases} = cachedState - - if (cachedReleases === activeReleasesCount) { - console.log('Using cached value.') - - return merge( - of(orgActiveReleaseCount), - timer(STATE_TTL_MS).pipe( - map(() => { - console.log('TTL expired, emitting null.') - resourceCache.set({ - namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, - dependencies: [activeReleases], - value: null, - }) - - stateTrigger$.next(null) - return null - }), - ), - ) - } - } - - return of(null) - }), - startWith(null), - ), - dispatch$, - ).pipe(shareReplay({bufferSize: 1, refCount: true})) - - const setOrgActiveReleaseCount = (orgActiveReleaseCount: number) => { - const activeReleasesCount = activeReleases?.length || 0 - - console.log('Storing orgActiveReleaseCount...') - resourceCache.set({ - namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, - dependencies: [activeReleases], - value: { - orgActiveReleaseCount, - activeReleases: activeReleasesCount, - }, - }) +interface ReleaseLimits { + orgActiveReleaseCount$: Observable +} - stateTrigger$.next(activeReleasesCount) - } +const STATE_TTL_MS = 15000 +const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount' + +function createOrgActiveReleaseCountStore(activeReleasesCount: number): ReleaseLimits { + const fetchTrigger$ = new BehaviorSubject<'fetch' | number | null>('fetch') + const staleFlag$ = new BehaviorSubject(false) + const countAtFetch$ = new BehaviorSubject(null) + + const orgActiveReleaseCount$ = fetchTrigger$.pipe( + switchMap((trigger) => { + if ( + trigger === 'fetch' || + staleFlag$.getValue() === true || + countAtFetch$.getValue() !== activeReleasesCount + ) { + staleFlag$.next(false) + + return fetchReleasesLimits().pipe( + tap(() => countAtFetch$.next(activeReleasesCount)), + map((res) => res.orgActiveReleaseCount), + switchMap((value) => { + fetchTrigger$.next(value) + + timer(STATE_TTL_MS).subscribe(() => { + console.log('TTL expired, marking cache as stale.') + staleFlag$.next(true) + countAtFetch$.next(null) + }) + + return of(value) + }), + ) + } + + return of(trigger) + }), + ) return { - state$, - setOrgActiveReleaseCount, + orgActiveReleaseCount$, } } @@ -98,15 +57,23 @@ export const useOrgActiveReleaseCount = () => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() - const {state$, setOrgActiveReleaseCount} = useMemo( - () => createOrgActiveReleaseCountStore(resourceCache, activeReleases), - [resourceCache, activeReleases], - ) + const activeReleasesCount = activeReleases?.length || 0 - const state = useObservable(state$, null) + const count = useMemo(() => ({activeReleasesCount}), [activeReleasesCount]) - return { - getOrgActiveReleaseCount: () => state, - setOrgActiveReleaseCount, - } + return useMemo(() => { + const releaseLimitsStore = + resourceCache.get({ + dependencies: [count], + namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, + }) || createOrgActiveReleaseCountStore(activeReleasesCount) + + resourceCache.set({ + namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, + value: releaseLimitsStore, + dependencies: [count], + }) + + return releaseLimitsStore + }, [activeReleasesCount, count, resourceCache]) } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 83c136d06ed..b994863d21f 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,118 +1,48 @@ import {useMemo} from 'react' -import {useObservable} from 'react-rx' -import {BehaviorSubject, firstValueFrom, scan, shareReplay} from 'rxjs' +import {map, type Observable, shareReplay} from 'rxjs' -import {type ResourceCache, useResourceCache} from '../../store/_legacy/ResourceCacheProvider' +import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' interface ReleaseLimits { - datasetReleaseLimit: number - orgActiveReleaseLimit: number | null -} - -interface FetchState { - isFetching: boolean - isError: boolean + releaseLimits$: Observable<{ + datasetReleaseLimit: number + orgActiveReleaseLimit: number | null + }> } const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' -const releaseLimits$ = new BehaviorSubject(null) -const fetchState$ = new BehaviorSubject>({}) +function createReleaseLimitsStore(): ReleaseLimits { + const releaseLimits$ = fetchReleasesLimits().pipe( + map((res) => ({ + datasetReleaseLimit: res.datasetReleaseLimit, + orgActiveReleaseLimit: res.orgActiveReleaseLimit, + })), + shareReplay(1), + ) -function createReleaseLimitsStore(resourceCache: ResourceCache) { - const state$ = releaseLimits$.pipe(shareReplay({bufferSize: 1, refCount: true})) + return { + releaseLimits$, + } +} - const fetchState = fetchState$.pipe( - scan, FetchState>( - (state, patch) => ({ - isFetching: patch.isFetching ?? state.isFetching, - isError: patch.isError ?? state.isError, - }), - {isFetching: false, isError: false}, - ), - shareReplay({bufferSize: 1, refCount: true}), - ) +export const useReleaseLimits: () => ReleaseLimits = () => { + const resourceCache = useResourceCache() - const fetchReleaseLimits = async () => { - const latestState = - releaseLimits$.getValue() ?? + return useMemo(() => { + const releaseLimitsStore = resourceCache.get({ - namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, dependencies: [], - }) - - if (latestState) { - console.log('Returning already cached ReleaseLimits') - releaseLimits$.next(latestState) - fetchState$.next({isError: false, isFetching: false}) - return latestState - } - - if (fetchState$.getValue()?.isFetching) { - console.log('Fetch already in progress, skipping...') - return null - } - - console.log('Fetching ReleaseLimits...') - fetchState$.next({isFetching: true, isError: false}) - - try { - const limits = await firstValueFrom(fetchReleasesLimits()) - - resourceCache.set({ namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, - dependencies: [], - value: limits, - }) - releaseLimits$.next(limits) - fetchState$.next({isFetching: false, isError: false}) - return limits - } catch (error) { - console.error('Error fetching ReleaseLimits:', error) - fetchState$.next({isFetching: false, isError: true}) - return null - } - } + }) || createReleaseLimitsStore() - const setReleaseLimits = (limits: ReleaseLimits) => { - console.log('Storing ReleaseLimits...') resourceCache.set({ namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, + value: releaseLimitsStore, dependencies: [], - value: limits, }) - releaseLimits$.next(limits) - fetchState$.next({isError: false, isFetching: false}) - } - - return { - state$, - fetchState$: fetchState, - setReleaseLimits, - fetchReleaseLimits, - } -} - -export const useReleaseLimits = () => { - const resourceCache = useResourceCache() - - const { - state$, - fetchState$: _fetchState$, - setReleaseLimits, - fetchReleaseLimits, - } = useMemo(() => createReleaseLimitsStore(resourceCache), [resourceCache]) - - const state = useObservable(state$, null) - const fetchState = useObservable(_fetchState$, {isFetching: false, isError: false}) - - return { - getReleaseLimits: () => state, - isFetching: fetchState.isFetching, - isError: fetchState.isError, - setReleaseLimits, - fetchReleaseLimits, - } + return releaseLimitsStore + }, [resourceCache]) } From b46b722df0d075d3687945c32abe4bd3bbae8c20 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 20:57:57 +0000 Subject: [PATCH 20/38] refactor: tidy to logic layout in ReleasesUpsellProvider --- .../upsell/ReleasesUpsellProvider.tsx | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 0ceacce7b22..ff478b7de35 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -195,14 +195,20 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { - if (mode === 'upsell') { + if (mode === 'default') { + return cb() + } + + const doUpsell = () => { handleOpenDialog() if (throwError) { throw new StudioReleaseLimitExceededError() } - return + return false } + if (mode === 'upsell') return doUpsell() + try { console.log('Guard called, checking caches...') // if either fails then catch the error @@ -215,35 +221,29 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { // orgActiveReleaseCount might be missing due to internal server error // allow pass through guard in that case - if (orgActiveReleaseCount) { - const activeReleasesCount = activeReleases?.length || 0 - - const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit - const isCurrentDatasetAtAboveOrgLimit = - orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit - const isOrgAtAboveOrgLimit = - orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit - - const shouldShowDialog = - isCurrentDatasetAtAboveDatasetLimit || - isCurrentDatasetAtAboveOrgLimit || - isOrgAtAboveOrgLimit - - if (shouldShowDialog) { - handleOpenDialog(orgActiveReleaseCount) - if (throwError) { - throw new StudioReleaseLimitExceededError() - } - return - } - } + if (orgActiveReleaseCount === null) return cb() + + const activeReleasesCount = activeReleases?.length || 0 + + const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit + const isCurrentDatasetAtAboveOrgLimit = + orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit + const isOrgAtAboveOrgLimit = + orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit + + const shouldShowDialog = + isCurrentDatasetAtAboveDatasetLimit || + isCurrentDatasetAtAboveOrgLimit || + isOrgAtAboveOrgLimit + + if (shouldShowDialog) return doUpsell() - cb() + return cb() } catch (e) { console.error('Error fetching release limits for upsell:', e) // silently fail and allow pass through guard - cb() + return cb() } }, [mode, handleOpenDialog, orgActiveReleaseCount$, releaseLimits$, activeReleases?.length], From 7a4ce41cba39d34f304fc49ed5feb6517d818348 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 22:04:09 +0000 Subject: [PATCH 21/38] fix: correcting issue that meant guard would always call cb --- .../upsell/ReleasesUpsellProvider.tsx | 12 ++++---- .../contexts/upsell/fetchReleaseLimits.ts | 29 +++++++++++++++++++ .../store/useOrgActiveReleaseCount.ts | 18 ++++++++---- .../core/releases/store/useReleaseLimits.ts | 15 ++++++---- 4 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index ff478b7de35..0023b90e567 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -195,12 +195,12 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { - if (mode === 'default') { - return cb() - } + // if (mode === 'default') { + // return cb() + // } - const doUpsell = () => { - handleOpenDialog() + const doUpsell = (count?: number) => { + handleOpenDialog(count) if (throwError) { throw new StudioReleaseLimitExceededError() } @@ -236,7 +236,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { isCurrentDatasetAtAboveOrgLimit || isOrgAtAboveOrgLimit - if (shouldShowDialog) return doUpsell() + if (shouldShowDialog) return doUpsell(orgActiveReleaseCount) return cb() } catch (e) { diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts new file mode 100644 index 00000000000..1d83cc95a3f --- /dev/null +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -0,0 +1,29 @@ +import {type SanityClient} from '@sanity/client' +import {type Observable} from 'rxjs' + +interface ReleaseLimits { + orgActiveReleaseCount: number + datasetReleaseLimit: number + orgActiveReleaseLimit: number | null +} + +/** + * @internal + * fetches subscriptions for this project + */ +export function fetchReleaseLimits({ + versionedClient, +}: { + versionedClient: SanityClient +}): Observable { + return versionedClient + .withConfig({ + useProjectHostname: false, + apiHost: 'https://api.sanity.work', + }) + .observable.request({ + uri: '/features', + // uri: `project/${versionedClient.config().projectId}/new-content-release-allowed`, + tag: 'new-content-release-allowed', + }) +} diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 24cd5072cb0..8425b53c645 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,7 +1,9 @@ /* eslint-disable no-console */ +import {type SanityClient} from '@sanity/client' import {useMemo} from 'react' import {BehaviorSubject, map, type Observable, of, switchMap, tap, timer} from 'rxjs' +import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' import {useActiveReleases} from './useActiveReleases' @@ -13,7 +15,10 @@ interface ReleaseLimits { const STATE_TTL_MS = 15000 const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount' -function createOrgActiveReleaseCountStore(activeReleasesCount: number): ReleaseLimits { +function createOrgActiveReleaseCountStore( + versionedClient: SanityClient, + activeReleasesCount: number, +): ReleaseLimits { const fetchTrigger$ = new BehaviorSubject<'fetch' | number | null>('fetch') const staleFlag$ = new BehaviorSubject(false) const countAtFetch$ = new BehaviorSubject(null) @@ -27,7 +32,7 @@ function createOrgActiveReleaseCountStore(activeReleasesCount: number): ReleaseL ) { staleFlag$.next(false) - return fetchReleasesLimits().pipe( + return fetchReleasesLimits({versionedClient}).pipe( tap(() => countAtFetch$.next(activeReleasesCount)), map((res) => res.orgActiveReleaseCount), switchMap((value) => { @@ -56,6 +61,7 @@ function createOrgActiveReleaseCountStore(activeReleasesCount: number): ReleaseL export const useOrgActiveReleaseCount = () => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() + const client = useClient() const activeReleasesCount = activeReleases?.length || 0 @@ -64,16 +70,16 @@ export const useOrgActiveReleaseCount = () => { return useMemo(() => { const releaseLimitsStore = resourceCache.get({ - dependencies: [count], + dependencies: [client, count], namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, - }) || createOrgActiveReleaseCountStore(activeReleasesCount) + }) || createOrgActiveReleaseCountStore(client, activeReleasesCount) resourceCache.set({ namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, value: releaseLimitsStore, - dependencies: [count], + dependencies: [client, count], }) return releaseLimitsStore - }, [activeReleasesCount, count, resourceCache]) + }, [activeReleasesCount, client, count, resourceCache]) } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index b994863d21f..3aacc239bcd 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,6 +1,8 @@ +import {type SanityClient} from '@sanity/client' import {useMemo} from 'react' import {map, type Observable, shareReplay} from 'rxjs' +import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' @@ -13,8 +15,8 @@ interface ReleaseLimits { const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' -function createReleaseLimitsStore(): ReleaseLimits { - const releaseLimits$ = fetchReleasesLimits().pipe( +function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits { + const releaseLimits$ = fetchReleasesLimits({versionedClient}).pipe( map((res) => ({ datasetReleaseLimit: res.datasetReleaseLimit, orgActiveReleaseLimit: res.orgActiveReleaseLimit, @@ -29,20 +31,21 @@ function createReleaseLimitsStore(): ReleaseLimits { export const useReleaseLimits: () => ReleaseLimits = () => { const resourceCache = useResourceCache() + const client = useClient({apiVersion: 'vX'}) return useMemo(() => { const releaseLimitsStore = resourceCache.get({ - dependencies: [], + dependencies: [client], namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, - }) || createReleaseLimitsStore() + }) || createReleaseLimitsStore(client) resourceCache.set({ namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, value: releaseLimitsStore, - dependencies: [], + dependencies: [client], }) return releaseLimitsStore - }, [resourceCache]) + }, [client, resourceCache]) } From cf057020383d6ea71f1a6841bd5fddef898bbfe5 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Tue, 18 Feb 2025 22:19:37 +0000 Subject: [PATCH 22/38] fix: resolving type errors --- .../core/releases/contexts/upsell/ReleasesUpsellProvider.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 0023b90e567..062e629fac1 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +import {type SanityClient} from '@sanity/client' import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' @@ -38,7 +39,7 @@ const BASE_URL = 'www.sanity.io' // Date when the change from array to object in the data returned was introduced. const API_VERSION = '2024-04-19' -export const fetchReleasesLimits = () => +export const fetchReleasesLimits = ({versionedClient}: {versionedClient: SanityClient}) => of({ orgActiveReleaseCount: 6, orgActiveReleaseLimit: 20, @@ -199,7 +200,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { // return cb() // } - const doUpsell = (count?: number) => { + const doUpsell: (count?: number) => false = (count) => { handleOpenDialog(count) if (throwError) { throw new StudioReleaseLimitExceededError() From bbcdde7e735fcbebaf83f2aa1a026d0bb5a512f9 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 19 Feb 2025 11:08:43 +0000 Subject: [PATCH 23/38] fix: resolving more TS issues --- .../upsell/ReleasesUpsellProvider.tsx | 87 +++++++++---------- .../core/releases/contexts/upsell/types.ts | 2 +- .../store/createReleaseOperationStore.ts | 6 +- .../core/releases/store/createReleaseStore.ts | 7 -- .../sanity/src/core/releases/store/types.ts | 1 - .../store/useOrgActiveReleaseCount.ts | 16 ++-- 6 files changed, 53 insertions(+), 66 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 062e629fac1..9f15a5d0488 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -70,7 +70,6 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const projectId = useProjectId() const telemetry = useTelemetry() const client = useClient({apiVersion: API_VERSION}) - const [_releaseLimit, setReleaseLimit] = useState(undefined) const {data: activeReleases} = useActiveReleases() const {enabled: isReleasesFeatureEnabled} = useFeatureEnabled('contentReleases') @@ -78,18 +77,15 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { /** * upsell if: * plan is free, ie releases is not feature enabled - * there is a limit and the limit is reached or exceeded */ - const isAtReleaseLimit = - !isReleasesFeatureEnabled || (_releaseLimit && (activeReleases?.length || 0) >= _releaseLimit) - if (isAtReleaseLimit && upsellData) { + if (!isReleasesFeatureEnabled && upsellData) { return 'upsell' } - if (isAtReleaseLimit && !upsellData) { + if (isReleasesFeatureEnabled && !upsellData) { return 'disabled' } return 'default' - }, [activeReleases?.length, isReleasesFeatureEnabled, _releaseLimit, upsellData]) + }, [isReleasesFeatureEnabled, upsellData]) const telemetryLogs = useMemo( (): ReleasesUpsellContextValue['telemetryLogs'] => ({ @@ -196,10 +192,6 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { - // if (mode === 'default') { - // return cb() - // } - const doUpsell: (count?: number) => false = (count) => { handleOpenDialog(count) if (throwError) { @@ -210,55 +202,56 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { if (mode === 'upsell') return doUpsell() - try { - console.log('Guard called, checking caches...') - // if either fails then catch the error - const [orgActiveReleaseCount, releaseLimits] = await Promise.all([ - firstValueFrom(orgActiveReleaseCount$), - firstValueFrom(releaseLimits$), - ]) + const fetchLimitsCount = async () => { + try { + console.log('Guard called, checking caches...') + // if either fails then catch the error + return await Promise.all([ + firstValueFrom(orgActiveReleaseCount$), + firstValueFrom(releaseLimits$), + ]) + } catch (e) { + console.error('Error fetching release limits for upsell:', e) - const {orgActiveReleaseLimit, datasetReleaseLimit} = releaseLimits + return null + } + } - // orgActiveReleaseCount might be missing due to internal server error - // allow pass through guard in that case - if (orgActiveReleaseCount === null) return cb() + const result = await fetchLimitsCount() - const activeReleasesCount = activeReleases?.length || 0 + // silently fail and allow pass through guard + if (result === null) return cb() - const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit - const isCurrentDatasetAtAboveOrgLimit = - orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit - const isOrgAtAboveOrgLimit = - orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit + const [orgActiveReleaseCount, releaseLimits] = result + const {orgActiveReleaseLimit, datasetReleaseLimit} = releaseLimits - const shouldShowDialog = - isCurrentDatasetAtAboveDatasetLimit || - isCurrentDatasetAtAboveOrgLimit || - isOrgAtAboveOrgLimit + // orgActiveReleaseCount might be missing due to internal server error + // allow pass through guard in that case + if (orgActiveReleaseCount === null) return cb() - if (shouldShowDialog) return doUpsell(orgActiveReleaseCount) + const activeReleasesCount = activeReleases?.length || 0 - return cb() - } catch (e) { - console.error('Error fetching release limits for upsell:', e) + const isCurrentDatasetAtAboveDatasetLimit = activeReleasesCount >= datasetReleaseLimit + const isCurrentDatasetAtAboveOrgLimit = + orgActiveReleaseLimit !== null && activeReleasesCount >= orgActiveReleaseLimit + const isOrgAtAboveOrgLimit = + orgActiveReleaseLimit !== null && orgActiveReleaseCount >= orgActiveReleaseLimit - // silently fail and allow pass through guard - return cb() - } + const shouldShowDialog = + isCurrentDatasetAtAboveDatasetLimit || + isCurrentDatasetAtAboveOrgLimit || + isOrgAtAboveOrgLimit + + if (shouldShowDialog) return doUpsell(orgActiveReleaseCount) + + return cb() }, [mode, handleOpenDialog, orgActiveReleaseCount$, releaseLimits$, activeReleases?.length], ) const onReleaseLimitReached = useCallback( - (limit: number, suppressDialogOpening: boolean = false) => { - setReleaseLimit(limit) - - if (!suppressDialogOpening && (activeReleases?.length || 0) >= limit) { - handleOpenDialog() - } - }, - [activeReleases?.length, handleOpenDialog], + (limit: number) => handleOpenDialog(limit), + [handleOpenDialog], ) const ctxValue = useMemo( diff --git a/packages/sanity/src/core/releases/contexts/upsell/types.ts b/packages/sanity/src/core/releases/contexts/upsell/types.ts index 945c62b2297..67019c55830 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/types.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/types.ts @@ -9,7 +9,7 @@ export interface ReleasesUpsellContextValue { mode: 'upsell' | 'default' | 'disabled' upsellDialogOpen: boolean guardWithReleaseLimitUpsell: (callback: () => void, throwError?: boolean) => Promise - onReleaseLimitReached: (limit: number, suppressDialogOpening: boolean) => void + onReleaseLimitReached: (limit: number) => void telemetryLogs: { dialogSecondaryClicked: () => void dialogPrimaryClicked: () => void diff --git a/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts b/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts index 0d98df737f5..f5a362f7f30 100644 --- a/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts +++ b/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts @@ -355,9 +355,11 @@ export function createRequestAction( }, }) } catch (e) { - if (isReleaseLimitError(e)) { + // if dryRunning then essentially this is a silent request + // so don't want to create disruptive upsell because of limit + if (!!options?.dryRun && isReleaseLimitError(e)) { // free accounts do not return limit, 0 is implied - onReleaseLimitReached(e.details.limit || 0, !!options?.dryRun) + onReleaseLimitReached(e.details.limit || 0) } throw e diff --git a/packages/sanity/src/core/releases/store/createReleaseStore.ts b/packages/sanity/src/core/releases/store/createReleaseStore.ts index c78a73365b6..5c69a280958 100644 --- a/packages/sanity/src/core/releases/store/createReleaseStore.ts +++ b/packages/sanity/src/core/releases/store/createReleaseStore.ts @@ -22,7 +22,6 @@ import {distinctUntilChanged, map, startWith} from 'rxjs/operators' import {type DocumentPreviewStore} from '../../preview' import {listenQuery} from '../../store/_legacy' -import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' import {RELEASE_DOCUMENT_TYPE, RELEASE_DOCUMENTS_PATH} from './constants' import {createReleaseMetadataAggregator} from './createReleaseMetadataAggregator' import {releasesReducer, type ReleasesReducerAction, type ReleasesReducerState} from './reducer' @@ -148,12 +147,10 @@ export function createReleaseStore(context: { ) const errorCount$ = state$.pipe(releaseStoreErrorCount(), shareReplay(1)) - const releaseLimits$ = releasesStoreLimits().pipe(shareReplay(1)) const getMetadataStateForSlugs$ = createReleaseMetadataAggregator(client) return { - releaseLimits$, state$, errorCount$, getMetadataStateForSlugs$, @@ -176,7 +173,3 @@ export function releaseStoreErrorCount(): OperatorFunction - releaseLimits$: Observable<{orgActiveReleaseLimit: number}> /** * Counts all loaded release documents that are in an active state and have an error recorded. * This is determined by the presence of the `error` field in the release document. diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 8425b53c645..43d70768415 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -19,14 +19,14 @@ function createOrgActiveReleaseCountStore( versionedClient: SanityClient, activeReleasesCount: number, ): ReleaseLimits { - const fetchTrigger$ = new BehaviorSubject<'fetch' | number | null>('fetch') + const latestFetchState = new BehaviorSubject(null) const staleFlag$ = new BehaviorSubject(false) const countAtFetch$ = new BehaviorSubject(null) - const orgActiveReleaseCount$ = fetchTrigger$.pipe( - switchMap((trigger) => { + const orgActiveReleaseCount$ = latestFetchState.pipe( + switchMap((state) => { if ( - trigger === 'fetch' || + state === null || staleFlag$.getValue() === true || countAtFetch$.getValue() !== activeReleasesCount ) { @@ -35,8 +35,8 @@ function createOrgActiveReleaseCountStore( return fetchReleasesLimits({versionedClient}).pipe( tap(() => countAtFetch$.next(activeReleasesCount)), map((res) => res.orgActiveReleaseCount), - switchMap((value) => { - fetchTrigger$.next(value) + switchMap((nextState) => { + latestFetchState.next(nextState) timer(STATE_TTL_MS).subscribe(() => { console.log('TTL expired, marking cache as stale.') @@ -44,12 +44,12 @@ function createOrgActiveReleaseCountStore( countAtFetch$.next(null) }) - return of(value) + return of(nextState) }), ) } - return of(trigger) + return of(state) }), ) From cdf4808518ab884d48842265f25940aa1eac439e Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 19 Feb 2025 11:22:06 +0000 Subject: [PATCH 24/38] fix: resolving onLimitReached cb to not exec if dryRun --- .../contexts/upsell/ReleasesUpsellProvider.tsx | 3 +++ .../src/core/releases/hooks/useIsReleasesPlus.ts | 2 +- .../releases/store/createReleaseOperationStore.ts | 2 +- .../core/releases/store/useOrgActiveReleaseCount.ts | 4 ++-- .../src/core/releases/store/useReleaseLimits.ts | 13 +++++++++---- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index 9f15a5d0488..fa76dc4ea3c 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -223,6 +223,9 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { if (result === null) return cb() const [orgActiveReleaseCount, releaseLimits] = result + + if (releaseLimits === null || orgActiveReleaseCount === null) return cb() + const {orgActiveReleaseLimit, datasetReleaseLimit} = releaseLimits // orgActiveReleaseCount might be missing due to internal server error diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index 2b7147b5d6d..2a8fdbf9b04 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -11,6 +11,6 @@ export const useIsReleasesPlus = () => { const {orgActiveReleaseLimit} = releaseLimit || {} - // presume not releases+ if empty data + // presume not releases+ if null releaseLimit return orgActiveReleaseLimit && orgActiveReleaseLimit >= RELEASES_PLUS_LIMIT } diff --git a/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts b/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts index f5a362f7f30..9814d17793e 100644 --- a/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts +++ b/packages/sanity/src/core/releases/store/createReleaseOperationStore.ts @@ -357,7 +357,7 @@ export function createRequestAction( } catch (e) { // if dryRunning then essentially this is a silent request // so don't want to create disruptive upsell because of limit - if (!!options?.dryRun && isReleaseLimitError(e)) { + if (!options?.dryRun && isReleaseLimitError(e)) { // free accounts do not return limit, 0 is implied onReleaseLimitReached(e.details.limit || 0) } diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 43d70768415..d4fc55ce780 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -5,7 +5,7 @@ import {BehaviorSubject, map, type Observable, of, switchMap, tap, timer} from ' import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' +import {fetchReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' import {useActiveReleases} from './useActiveReleases' interface ReleaseLimits { @@ -32,7 +32,7 @@ function createOrgActiveReleaseCountStore( ) { staleFlag$.next(false) - return fetchReleasesLimits({versionedClient}).pipe( + return fetchReleaseLimits({versionedClient}).pipe( tap(() => countAtFetch$.next(activeReleasesCount)), map((res) => res.orgActiveReleaseCount), switchMap((nextState) => { diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 3aacc239bcd..7981b1c83d3 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -1,27 +1,32 @@ import {type SanityClient} from '@sanity/client' import {useMemo} from 'react' -import {map, type Observable, shareReplay} from 'rxjs' +import {catchError, map, type Observable, of, shareReplay} from 'rxjs' import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleasesLimits} from '../contexts/upsell/ReleasesUpsellProvider' +import {fetchReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' interface ReleaseLimits { releaseLimits$: Observable<{ datasetReleaseLimit: number orgActiveReleaseLimit: number | null - }> + } | null> } const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits { - const releaseLimits$ = fetchReleasesLimits({versionedClient}).pipe( + const releaseLimits$ = fetchReleaseLimits({versionedClient}).pipe( map((res) => ({ datasetReleaseLimit: res.datasetReleaseLimit, orgActiveReleaseLimit: res.orgActiveReleaseLimit, })), shareReplay(1), + catchError((error) => { + console.error('Failed to fetch release limits', error) + + return of(null) + }), ) return { From a094e3d69b1e849ca2847f484558078b576470da Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 19 Feb 2025 11:22:33 +0000 Subject: [PATCH 25/38] chore: toggle stub or real API --- .../releases/contexts/upsell/fetchReleaseLimits.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index 1d83cc95a3f..a26c8e2114d 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -1,17 +1,21 @@ import {type SanityClient} from '@sanity/client' import {type Observable} from 'rxjs' +import {fetchReleasesLimits} from './ReleasesUpsellProvider' + interface ReleaseLimits { orgActiveReleaseCount: number datasetReleaseLimit: number orgActiveReleaseLimit: number | null } +const USE_STUB = false + /** * @internal * fetches subscriptions for this project */ -export function fetchReleaseLimits({ +export function _fetchReleaseLimits({ versionedClient, }: { versionedClient: SanityClient @@ -22,8 +26,9 @@ export function fetchReleaseLimits({ apiHost: 'https://api.sanity.work', }) .observable.request({ - uri: '/features', - // uri: `project/${versionedClient.config().projectId}/new-content-release-allowed`, + uri: `project/${versionedClient.config().projectId}/new-content-release-allowed`, tag: 'new-content-release-allowed', }) } + +export const fetchReleaseLimits = USE_STUB ? fetchReleasesLimits : _fetchReleaseLimits From 1ffffd6705c1b35349174b34412fba8ad81471b6 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 19 Feb 2025 12:44:03 +0000 Subject: [PATCH 26/38] feat: using actual data response from API --- .../upsell/ReleasesUpsellProvider.tsx | 27 +----------- .../contexts/upsell/fetchReleaseLimits.ts | 43 ++++++++++++++----- .../core/releases/hooks/useIsReleasesPlus.ts | 4 ++ .../store/useOrgActiveReleaseCount.ts | 32 +++++++++++--- .../core/releases/store/useReleaseLimits.ts | 16 +++++-- .../tool/overview/ReleasesOverview.tsx | 14 +++--- 6 files changed, 84 insertions(+), 52 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index fa76dc4ea3c..ccba6d885e9 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -1,10 +1,8 @@ /* eslint-disable no-console */ -import {type SanityClient} from '@sanity/client' import {useTelemetry} from '@sanity/telemetry/react' import {template} from 'lodash' import {useCallback, useEffect, useMemo, useState} from 'react' -import {firstValueFrom, of} from 'rxjs' -import {delay, shareReplay, tap} from 'rxjs/operators' +import {firstValueFrom} from 'rxjs' import {ReleasesUpsellContext} from 'sanity/_singletons' import {useClient, useFeatureEnabled, useProjectId} from '../../../hooks' @@ -39,27 +37,6 @@ const BASE_URL = 'www.sanity.io' // Date when the change from array to object in the data returned was introduced. const API_VERSION = '2024-04-19' -export const fetchReleasesLimits = ({versionedClient}: {versionedClient: SanityClient}) => - of({ - orgActiveReleaseCount: 6, - orgActiveReleaseLimit: 20, - datasetReleaseLimit: 6, - - // orgActiveReleaseCount: 6, - // orgActiveReleaseLimit: 6, - // datasetReleaseLimit: 10, - }).pipe( - shareReplay(1), - tap(() => console.log('fetchReleasesLimits')), - delay(3000), - ) - -// export const fetchReleasesLimits = () => -// throwError(() => new Error('Simulated API failure')).pipe( -// tap(() => console.log('fetchReleasesLimits - Simulating failure')), -// delay(3000), -// ) - /** * @beta * @hidden @@ -211,7 +188,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { firstValueFrom(releaseLimits$), ]) } catch (e) { - console.error('Error fetching release limits for upsell:', e) + console.error('Error fetching release limits and org count for upsell:', e) return null } diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index a26c8e2114d..fae19a314ac 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -1,7 +1,6 @@ +/* eslint-disable no-console */ import {type SanityClient} from '@sanity/client' -import {type Observable} from 'rxjs' - -import {fetchReleasesLimits} from './ReleasesUpsellProvider' +import {delay, map, type Observable, of, tap} from 'rxjs' interface ReleaseLimits { orgActiveReleaseCount: number @@ -9,26 +8,48 @@ interface ReleaseLimits { orgActiveReleaseLimit: number | null } +interface ReleaseLimitsResponse { + data: ReleaseLimits +} + const USE_STUB = false +const stubFetchReleasesLimits = ({versionedClient}: {versionedClient: SanityClient}) => + of({ + orgActiveReleaseCount: 6, + orgActiveReleaseLimit: 20, + datasetReleaseLimit: 6, + + // orgActiveReleaseCount: 6, + // orgActiveReleaseLimit: 6, + // datasetReleaseLimit: 10, + }).pipe( + tap(() => console.log('fetchReleasesLimits')), + delay(3000), + ) + +// export const stubFetchReleasesLimits = () => +// throwError(() => new Error('Simulated API failure')).pipe( +// tap(() => console.log('fetchReleasesLimits - Simulating failure')), +// delay(3000), +// ) + /** * @internal * fetches subscriptions for this project */ +// export function fetchReleaseLimits({ export function _fetchReleaseLimits({ versionedClient, }: { versionedClient: SanityClient }): Observable { - return versionedClient - .withConfig({ - useProjectHostname: false, - apiHost: 'https://api.sanity.work', - }) - .observable.request({ - uri: `project/${versionedClient.config().projectId}/new-content-release-allowed`, + return versionedClient.observable + .request({ + uri: `projects/${versionedClient.config().projectId}/new-content-release-allowed`, tag: 'new-content-release-allowed', }) + .pipe(map((response) => response.data)) } -export const fetchReleaseLimits = USE_STUB ? fetchReleasesLimits : _fetchReleaseLimits +export const fetchReleaseLimits = USE_STUB ? stubFetchReleasesLimits : _fetchReleaseLimits diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index 2a8fdbf9b04..2ec9deb0101 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -4,6 +4,10 @@ import {useReleaseLimits} from '../store/useReleaseLimits' const RELEASES_PLUS_LIMIT = 2 +/** + * @internal + * @returns `boolean` Whether the current org is on a Releases+ plan + */ export const useIsReleasesPlus = () => { const {releaseLimits$} = useReleaseLimits() diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index d4fc55ce780..163585f3fae 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import {type SanityClient} from '@sanity/client' import {useMemo} from 'react' -import {BehaviorSubject, map, type Observable, of, switchMap, tap, timer} from 'rxjs' +import {BehaviorSubject, catchError, map, type Observable, of, switchMap, tap, timer} from 'rxjs' import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' @@ -21,27 +21,35 @@ function createOrgActiveReleaseCountStore( ): ReleaseLimits { const latestFetchState = new BehaviorSubject(null) const staleFlag$ = new BehaviorSubject(false) - const countAtFetch$ = new BehaviorSubject(null) + const activeReleaseCountAtFetch = new BehaviorSubject(null) const orgActiveReleaseCount$ = latestFetchState.pipe( switchMap((state) => { if ( state === null || staleFlag$.getValue() === true || - countAtFetch$.getValue() !== activeReleasesCount + activeReleaseCountAtFetch.getValue() !== activeReleasesCount ) { staleFlag$.next(false) return fetchReleaseLimits({versionedClient}).pipe( - tap(() => countAtFetch$.next(activeReleasesCount)), - map((res) => res.orgActiveReleaseCount), + tap(() => activeReleaseCountAtFetch.next(activeReleasesCount)), + map((data) => data.orgActiveReleaseCount), + catchError((error) => { + console.error('Failed to fetch org release count', error) + + if (!state) throw error + + // return the last state if it exists + return of(state) + }), switchMap((nextState) => { latestFetchState.next(nextState) timer(STATE_TTL_MS).subscribe(() => { console.log('TTL expired, marking cache as stale.') staleFlag$.next(true) - countAtFetch$.next(null) + activeReleaseCountAtFetch.next(null) }) return of(nextState) @@ -58,10 +66,20 @@ function createOrgActiveReleaseCountStore( } } +/** + * @internal + * + * Returns a shared observable to a cache of the org's active release count. + * + * This cache expires after a TTL or whenever the active releases in the current + * dataset changes. + * + * @returns An Observable of the cached value for org's active release count. + */ export const useOrgActiveReleaseCount = () => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() - const client = useClient() + const client = useClient({apiVersion: 'vX'}) const activeReleasesCount = activeReleases?.length || 0 diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 7981b1c83d3..11987411b64 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -17,9 +17,9 @@ const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits { const releaseLimits$ = fetchReleaseLimits({versionedClient}).pipe( - map((res) => ({ - datasetReleaseLimit: res.datasetReleaseLimit, - orgActiveReleaseLimit: res.orgActiveReleaseLimit, + map((data) => ({ + datasetReleaseLimit: data.datasetReleaseLimit, + orgActiveReleaseLimit: data.orgActiveReleaseLimit, })), shareReplay(1), catchError((error) => { @@ -34,6 +34,16 @@ function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits } } +/** + * @internal + * + * Returns a shared observable to a cache of the release limits for the current project. + * + * This cache is shared across all instances of this hook, and will only be fetched once. + * It will never expire as the limits are not expected to change during the lifetime of the render cycle. + * + * @returns An Observable of the cached value for the release limits + */ export const useReleaseLimits: () => ReleaseLimits = () => { const resourceCache = useResourceCache() const client = useClient({apiVersion: 'vX'}) diff --git a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx index 4d469b79ee2..4b3552c4da5 100644 --- a/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx +++ b/packages/sanity/src/core/releases/tool/overview/ReleasesOverview.tsx @@ -90,6 +90,7 @@ export function ReleasesOverview() { const {createRelease} = useReleaseOperations() const {checkWithPermissionGuard} = useReleasePermissions() const [hasCreatePermission, setHasCreatePermission] = useState(null) + const [isPendingGuardResponse, setIsPendingGuardResponse] = useState(false) const mediaIndex = useMediaIndex() @@ -241,14 +242,12 @@ export function ReleasesOverview() { archivedReleases.length, ]) - const [isPendingGuard, setIsPendingGuard] = useState(false) - const handleOnClickCreateRelease = useCallback(async () => { - setIsPendingGuard(true) + setIsPendingGuardResponse(true) await guardWithReleaseLimitUpsell(() => { setIsCreateReleaseDialogOpen(true) }) - setIsPendingGuard(false) + setIsPendingGuardResponse(false) }, [guardWithReleaseLimitUpsell]) const createReleaseButton = useMemo( @@ -256,7 +255,10 @@ export function ReleasesOverview() { ), [ - isPendingGuard, + isPendingGuardResponse, hasCreatePermission, isCreateReleaseDialogOpen, mode, From bcc45fea2fc83fd17388fead4b0f0823078ccb27 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 19 Feb 2025 14:53:31 +0000 Subject: [PATCH 27/38] fix: handling error from backend that still has data --- .../releases/contexts/upsell/fetchReleaseLimits.ts | 14 +++++++++++--- .../releases/store/useOrgActiveReleaseCount.ts | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index fae19a314ac..af91fc3403f 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -import {type SanityClient} from '@sanity/client' -import {delay, map, type Observable, of, tap} from 'rxjs' +import {type ClientError, type SanityClient} from '@sanity/client' +import {catchError, delay, map, type Observable, of, tap} from 'rxjs' interface ReleaseLimits { orgActiveReleaseCount: number @@ -49,7 +49,15 @@ export function _fetchReleaseLimits({ uri: `projects/${versionedClient.config().projectId}/new-content-release-allowed`, tag: 'new-content-release-allowed', }) - .pipe(map((response) => response.data)) + .pipe( + catchError((error: ClientError) => { + console.error(error.message) + // body will still contain the limits and current count (if available) + // so still want to return these and just silently log the error + return of(error.response.body as ReleaseLimitsResponse) + }), + map(({data}) => data), + ) } export const fetchReleaseLimits = USE_STUB ? stubFetchReleasesLimits : _fetchReleaseLimits diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 163585f3fae..068657b0f8f 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -12,7 +12,8 @@ interface ReleaseLimits { orgActiveReleaseCount$: Observable } -const STATE_TTL_MS = 15000 +// @todo make this 60_000 +const STATE_TTL_MS = 15_000 const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount' function createOrgActiveReleaseCountStore( From c03e227d653a043221c9a9d0886a9e089fa23e8d Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Wed, 19 Feb 2025 17:14:37 +0000 Subject: [PATCH 28/38] feat: using the defaultActiveLimit to determine releases+ --- .../src/core/releases/contexts/upsell/fetchReleaseLimits.ts | 4 +++- .../sanity/src/core/releases/hooks/useIsReleasesPlus.ts | 6 ++---- packages/sanity/src/core/releases/store/useReleaseLimits.ts | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index af91fc3403f..5489badb7d1 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -3,6 +3,7 @@ import {type ClientError, type SanityClient} from '@sanity/client' import {catchError, delay, map, type Observable, of, tap} from 'rxjs' interface ReleaseLimits { + defaultOrgActiveReleaseLimit: number orgActiveReleaseCount: number datasetReleaseLimit: number orgActiveReleaseLimit: number | null @@ -16,6 +17,7 @@ const USE_STUB = false const stubFetchReleasesLimits = ({versionedClient}: {versionedClient: SanityClient}) => of({ + defaultOrgActiveReleaseLimit: 2, orgActiveReleaseCount: 6, orgActiveReleaseLimit: 20, datasetReleaseLimit: 6, @@ -23,7 +25,7 @@ const stubFetchReleasesLimits = ({versionedClient}: {versionedClient: SanityClie // orgActiveReleaseCount: 6, // orgActiveReleaseLimit: 6, // datasetReleaseLimit: 10, - }).pipe( + } as ReleaseLimits).pipe( tap(() => console.log('fetchReleasesLimits')), delay(3000), ) diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index 2ec9deb0101..0202d94b973 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -2,8 +2,6 @@ import {useObservable} from 'react-rx' import {useReleaseLimits} from '../store/useReleaseLimits' -const RELEASES_PLUS_LIMIT = 2 - /** * @internal * @returns `boolean` Whether the current org is on a Releases+ plan @@ -13,8 +11,8 @@ export const useIsReleasesPlus = () => { const releaseLimit = useObservable(releaseLimits$, null) - const {orgActiveReleaseLimit} = releaseLimit || {} + const {orgActiveReleaseLimit, defaultOrgActiveReleaseLimit = 0} = releaseLimit || {} // presume not releases+ if null releaseLimit - return orgActiveReleaseLimit && orgActiveReleaseLimit >= RELEASES_PLUS_LIMIT + return orgActiveReleaseLimit && orgActiveReleaseLimit >= defaultOrgActiveReleaseLimit } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 11987411b64..c93cf8f4fe4 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -8,6 +8,7 @@ import {fetchReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' interface ReleaseLimits { releaseLimits$: Observable<{ + defaultOrgActiveReleaseLimit: number datasetReleaseLimit: number orgActiveReleaseLimit: number | null } | null> @@ -18,6 +19,7 @@ const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits { const releaseLimits$ = fetchReleaseLimits({versionedClient}).pipe( map((data) => ({ + defaultOrgActiveReleaseLimit: data.defaultOrgActiveReleaseLimit, datasetReleaseLimit: data.datasetReleaseLimit, orgActiveReleaseLimit: data.orgActiveReleaseLimit, })), From 09f66ec8b8298ecc0023b2526b95ef8e6ae45df3 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 04:18:28 +0000 Subject: [PATCH 29/38] refactor: minor clarification on comments; reverting changes to interpolation for UpsellDialog --- .../src/core/releases/hooks/useIsReleasesPlus.ts | 1 + .../sanity/src/core/studio/upsell/UpsellDialog.tsx | 12 +----------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index 0202d94b973..55d24219d34 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -14,5 +14,6 @@ export const useIsReleasesPlus = () => { const {orgActiveReleaseLimit, defaultOrgActiveReleaseLimit = 0} = releaseLimit || {} // presume not releases+ if null releaseLimit + // (because of internal server error or network error) return orgActiveReleaseLimit && orgActiveReleaseLimit >= defaultOrgActiveReleaseLimit } diff --git a/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx b/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx index b081e9567c2..5375104418c 100644 --- a/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx +++ b/packages/sanity/src/core/studio/upsell/UpsellDialog.tsx @@ -3,7 +3,6 @@ import {CloseIcon, LaunchIcon} from '@sanity/icons' import {Box, Stack} from '@sanity/ui' // eslint-disable-next-line camelcase import {getTheme_v2} from '@sanity/ui/theme' -import {useEffect, useState} from 'react' import {styled} from 'styled-components' import {Button, Dialog} from '../../../ui-components' @@ -48,15 +47,6 @@ interface UpsellDialogProps { export function UpsellDialog(props: UpsellDialogProps) { const {data, onClose, onPrimaryClick, onSecondaryClick, interpolation} = props - const [stableInterpolation, setStableInterpolation] = useState(interpolation) - - useEffect(() => { - // prevent interpolation from resetting on re-render - // so that it keeps the value it was first rendered with - if (interpolation !== undefined) { - setStableInterpolation(interpolation) - } - }, [interpolation]) return ( From 440c4b0220c1d19f3fb74d5efd31db0f1789570d Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 04:18:54 +0000 Subject: [PATCH 30/38] refactor: reusing single type from fetchReleaseLimits; handling 404 errors gracefully --- .../contexts/upsell/fetchReleaseLimits.ts | 51 ++++++------------- .../store/useOrgActiveReleaseCount.ts | 11 ++-- .../core/releases/store/useReleaseLimits.ts | 17 +++---- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index 5489badb7d1..0b1477e20b4 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -1,11 +1,11 @@ -/* eslint-disable no-console */ import {type ClientError, type SanityClient} from '@sanity/client' -import {catchError, delay, map, type Observable, of, tap} from 'rxjs' +import {catchError, map, type Observable, of} from 'rxjs' -interface ReleaseLimits { - defaultOrgActiveReleaseLimit: number +export interface ReleaseLimits { orgActiveReleaseCount: number + defaultOrgActiveReleaseLimit: number datasetReleaseLimit: number + // internal server error has no fallback number - it uses null orgActiveReleaseLimit: number | null } @@ -13,35 +13,11 @@ interface ReleaseLimitsResponse { data: ReleaseLimits } -const USE_STUB = false - -const stubFetchReleasesLimits = ({versionedClient}: {versionedClient: SanityClient}) => - of({ - defaultOrgActiveReleaseLimit: 2, - orgActiveReleaseCount: 6, - orgActiveReleaseLimit: 20, - datasetReleaseLimit: 6, - - // orgActiveReleaseCount: 6, - // orgActiveReleaseLimit: 6, - // datasetReleaseLimit: 10, - } as ReleaseLimits).pipe( - tap(() => console.log('fetchReleasesLimits')), - delay(3000), - ) - -// export const stubFetchReleasesLimits = () => -// throwError(() => new Error('Simulated API failure')).pipe( -// tap(() => console.log('fetchReleasesLimits - Simulating failure')), -// delay(3000), -// ) - /** * @internal * fetches subscriptions for this project */ -// export function fetchReleaseLimits({ -export function _fetchReleaseLimits({ +export function fetchReleaseLimits({ versionedClient, }: { versionedClient: SanityClient @@ -53,13 +29,18 @@ export function _fetchReleaseLimits({ }) .pipe( catchError((error: ClientError) => { - console.error(error.message) - // body will still contain the limits and current count (if available) - // so still want to return these and just silently log the error - return of(error.response.body as ReleaseLimitsResponse) + console.error(error) + + if (typeof error.response.body !== 'string' && 'data' in error.response.body) { + // body will still contain the limits and current count (if available) + // so still want to return these and just silently log the error + return of(error.response.body as ReleaseLimitsResponse) + } + + // for internal server errors, or as a fallback + // propagate up the error + throw error }), map(({data}) => data), ) } - -export const fetchReleaseLimits = USE_STUB ? stubFetchReleasesLimits : _fetchReleaseLimits diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 068657b0f8f..b8f526c94cb 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -5,11 +5,11 @@ import {BehaviorSubject, catchError, map, type Observable, of, switchMap, tap, t import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' +import {fetchReleaseLimits, type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' import {useActiveReleases} from './useActiveReleases' -interface ReleaseLimits { - orgActiveReleaseCount$: Observable +interface OrgActiveReleaseCountStore { + orgActiveReleaseCount$: Observable } // @todo make this 60_000 @@ -19,7 +19,7 @@ const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount function createOrgActiveReleaseCountStore( versionedClient: SanityClient, activeReleasesCount: number, -): ReleaseLimits { +): OrgActiveReleaseCountStore { const latestFetchState = new BehaviorSubject(null) const staleFlag$ = new BehaviorSubject(false) const activeReleaseCountAtFetch = new BehaviorSubject(null) @@ -80,6 +80,7 @@ function createOrgActiveReleaseCountStore( export const useOrgActiveReleaseCount = () => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() + // @todo use default API version const client = useClient({apiVersion: 'vX'}) const activeReleasesCount = activeReleases?.length || 0 @@ -88,7 +89,7 @@ export const useOrgActiveReleaseCount = () => { return useMemo(() => { const releaseLimitsStore = - resourceCache.get({ + resourceCache.get({ dependencies: [client, count], namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, }) || createOrgActiveReleaseCountStore(client, activeReleasesCount) diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index c93cf8f4fe4..e6419065f9f 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -4,19 +4,15 @@ import {catchError, map, type Observable, of, shareReplay} from 'rxjs' import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' +import {fetchReleaseLimits, type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' -interface ReleaseLimits { - releaseLimits$: Observable<{ - defaultOrgActiveReleaseLimit: number - datasetReleaseLimit: number - orgActiveReleaseLimit: number | null - } | null> +interface ReleaseLimitsStore { + releaseLimits$: Observable | null> } const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' -function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits { +function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimitsStore { const releaseLimits$ = fetchReleaseLimits({versionedClient}).pipe( map((data) => ({ defaultOrgActiveReleaseLimit: data.defaultOrgActiveReleaseLimit, @@ -46,13 +42,14 @@ function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimits * * @returns An Observable of the cached value for the release limits */ -export const useReleaseLimits: () => ReleaseLimits = () => { +export const useReleaseLimits: () => ReleaseLimitsStore = () => { const resourceCache = useResourceCache() + // @todo use default API version const client = useClient({apiVersion: 'vX'}) return useMemo(() => { const releaseLimitsStore = - resourceCache.get({ + resourceCache.get({ dependencies: [client], namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, }) || createReleaseLimitsStore(client) From 41ee61b4a1f189840ad99d5626b68079078ffe77 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 10:45:07 +0000 Subject: [PATCH 31/38] fix: Revert button is disabled whilst determining release limits --- .../src/core/releases/hooks/useIsReleasesPlus.ts | 4 ++-- .../ReleaseRevertButton/ReleaseRevertButton.tsx | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index 55d24219d34..dbdf254fdb5 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -6,7 +6,7 @@ import {useReleaseLimits} from '../store/useReleaseLimits' * @internal * @returns `boolean` Whether the current org is on a Releases+ plan */ -export const useIsReleasesPlus = () => { +export const useIsReleasesPlus = (): boolean => { const {releaseLimits$} = useReleaseLimits() const releaseLimit = useObservable(releaseLimits$, null) @@ -15,5 +15,5 @@ export const useIsReleasesPlus = () => { // presume not releases+ if null releaseLimit // (because of internal server error or network error) - return orgActiveReleaseLimit && orgActiveReleaseLimit >= defaultOrgActiveReleaseLimit + return !!orgActiveReleaseLimit && orgActiveReleaseLimit >= defaultOrgActiveReleaseLimit } diff --git a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx index fe0ecfd50d4..8bb9dd22fd7 100644 --- a/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx +++ b/packages/sanity/src/core/releases/tool/components/releaseCTAButtons/ReleaseRevertButton/ReleaseRevertButton.tsx @@ -221,11 +221,13 @@ export const ReleaseRevertButton = ({ const {t} = useTranslation(releasesLocaleNamespace) const {guardWithReleaseLimitUpsell, mode} = useReleasesUpsell() const [revertReleaseStatus, setRevertReleaseStatus] = useState('idle') + const [isPendingGuardResponse, setIsPendingGuardResponse] = useState(false) - const handleMoveToConfirmStatus = useCallback( - () => guardWithReleaseLimitUpsell(() => setRevertReleaseStatus('confirm')), - [guardWithReleaseLimitUpsell], - ) + const handleMoveToConfirmStatus = useCallback(async () => { + setIsPendingGuardResponse(true) + await guardWithReleaseLimitUpsell(() => setRevertReleaseStatus('confirm')) + setIsPendingGuardResponse(false) + }, [guardWithReleaseLimitUpsell]) const isReleasesPlus = useIsReleasesPlus() @@ -238,7 +240,7 @@ export const ReleaseRevertButton = ({ onClick={handleMoveToConfirmStatus} text={t('action.revert')} tone="critical" - disabled={disabled || mode === 'disabled'} + disabled={isPendingGuardResponse || disabled || mode === 'disabled'} /> {revertReleaseStatus !== 'idle' && ( Date: Thu, 20 Feb 2025 11:51:10 +0000 Subject: [PATCH 32/38] chore: clarifying vX usage for `new-content-release-allowed` --- .../contexts/upsell/fetchReleaseLimits.ts | 19 ++++++++++++------- .../store/useOrgActiveReleaseCount.ts | 9 +++------ .../core/releases/store/useReleaseLimits.ts | 7 +++---- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index 0b1477e20b4..e307c2027a6 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -17,14 +17,19 @@ interface ReleaseLimitsResponse { * @internal * fetches subscriptions for this project */ -export function fetchReleaseLimits({ - versionedClient, -}: { - versionedClient: SanityClient -}): Observable { - return versionedClient.observable +export function fetchReleaseLimits(client: SanityClient): Observable { + const {projectId} = client.config() + + // This endpoint is prone to optimisation and further work + // it will never live within a versions API and will always be on vX + // until it goes away - there is graceful handling in `catchError` + // for when this endpoint is no longer available and limits are fetched + // some other way + const clientX = client.withConfig({apiVersion: 'vX'}) + + return clientX.observable .request({ - uri: `projects/${versionedClient.config().projectId}/new-content-release-allowed`, + uri: `projects/${projectId}/new-content-release-allowed`, tag: 'new-content-release-allowed', }) .pipe( diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index b8f526c94cb..5859ab66351 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import {type SanityClient} from '@sanity/client' import {useMemo} from 'react' import {BehaviorSubject, catchError, map, type Observable, of, switchMap, tap, timer} from 'rxjs' @@ -17,7 +16,7 @@ const STATE_TTL_MS = 15_000 const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount' function createOrgActiveReleaseCountStore( - versionedClient: SanityClient, + client: SanityClient, activeReleasesCount: number, ): OrgActiveReleaseCountStore { const latestFetchState = new BehaviorSubject(null) @@ -33,7 +32,7 @@ function createOrgActiveReleaseCountStore( ) { staleFlag$.next(false) - return fetchReleaseLimits({versionedClient}).pipe( + return fetchReleaseLimits(client).pipe( tap(() => activeReleaseCountAtFetch.next(activeReleasesCount)), map((data) => data.orgActiveReleaseCount), catchError((error) => { @@ -48,7 +47,6 @@ function createOrgActiveReleaseCountStore( latestFetchState.next(nextState) timer(STATE_TTL_MS).subscribe(() => { - console.log('TTL expired, marking cache as stale.') staleFlag$.next(true) activeReleaseCountAtFetch.next(null) }) @@ -80,8 +78,7 @@ function createOrgActiveReleaseCountStore( export const useOrgActiveReleaseCount = () => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() - // @todo use default API version - const client = useClient({apiVersion: 'vX'}) + const client = useClient() const activeReleasesCount = activeReleases?.length || 0 diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index e6419065f9f..e43438b44bb 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -12,8 +12,8 @@ interface ReleaseLimitsStore { const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' -function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimitsStore { - const releaseLimits$ = fetchReleaseLimits({versionedClient}).pipe( +function createReleaseLimitsStore(client: SanityClient): ReleaseLimitsStore { + const releaseLimits$ = fetchReleaseLimits(client).pipe( map((data) => ({ defaultOrgActiveReleaseLimit: data.defaultOrgActiveReleaseLimit, datasetReleaseLimit: data.datasetReleaseLimit, @@ -44,8 +44,7 @@ function createReleaseLimitsStore(versionedClient: SanityClient): ReleaseLimitsS */ export const useReleaseLimits: () => ReleaseLimitsStore = () => { const resourceCache = useResourceCache() - // @todo use default API version - const client = useClient({apiVersion: 'vX'}) + const client = useClient() return useMemo(() => { const releaseLimitsStore = From 41e308a3c526c128e1124af020387df0240189c1 Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 11:51:27 +0000 Subject: [PATCH 33/38] fix: case where interpolation on Upsell should work for zero values --- .../core/releases/contexts/upsell/ReleasesUpsellProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index ccba6d885e9..f18b2893267 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -245,7 +245,7 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [mode, upsellDialogOpen, guardWithReleaseLimitUpsell, onReleaseLimitReached, telemetryLogs], ) - const interpolation = releaseCount ? {releaseLimit: releaseCount} : undefined + const interpolation = releaseCount === null ? undefined : {releaseLimit: releaseCount} return ( From 00357f0729d8e94e9d7127a46dd3b75811fa266b Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 12:59:18 +0000 Subject: [PATCH 34/38] fix: studios without content releases feature enabled don't do permission checks call (assume permissions) --- .../releases/store/createReleasePermissionsStore.ts | 8 +++++++- .../src/core/releases/store/useReleasePermissions.ts | 10 ++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts b/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts index 722e4b85cc9..a91b4794d3c 100644 --- a/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts +++ b/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts @@ -22,7 +22,9 @@ export const isReleasePermissionError = (error: unknown): error is ReleasePermis * * @internal */ -export function createReleasePermissionsStore(): useReleasePermissionsValue { +export function createReleasePermissionsStore( + isContentReleasesEnabled: boolean, +): useReleasePermissionsValue { let permissions: {[key: string]: boolean} = {} /** @@ -36,6 +38,10 @@ export function createReleasePermissionsStore(): useReleasePermissionsValue { action: T, ...args: Parameters ): Promise => { + if (!isContentReleasesEnabled) { + return true + } + if (permissions[action.name] === undefined) { try { await action(...args, { diff --git a/packages/sanity/src/core/releases/store/useReleasePermissions.ts b/packages/sanity/src/core/releases/store/useReleasePermissions.ts index 9c1f66aebde..51eab5bc08a 100644 --- a/packages/sanity/src/core/releases/store/useReleasePermissions.ts +++ b/packages/sanity/src/core/releases/store/useReleasePermissions.ts @@ -1,5 +1,6 @@ import {useMemo} from 'react' +import {useFeatureEnabled} from '../../hooks/useFeatureEnabled' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {createReleasePermissionsStore} from './createReleasePermissionsStore' @@ -18,20 +19,21 @@ export interface useReleasePermissionsValue { */ export function useReleasePermissions(): useReleasePermissionsValue { const resourceCache = useResourceCache() + const contentReleasesFeature = useFeatureEnabled('contentReleases') return useMemo(() => { const releasePermissionsStore = resourceCache.get({ - dependencies: [], + dependencies: [contentReleasesFeature], namespace: RELEASE_PERMISSIONS_RESOURCE_CACHE_NAMESPACE, - }) || createReleasePermissionsStore() + }) || createReleasePermissionsStore(contentReleasesFeature.enabled) resourceCache.set({ namespace: RELEASE_PERMISSIONS_RESOURCE_CACHE_NAMESPACE, value: releasePermissionsStore, - dependencies: [], + dependencies: [contentReleasesFeature], }) return releasePermissionsStore - }, [resourceCache]) + }, [contentReleasesFeature, resourceCache]) } From 5d8bbddcddc777d4c1d0837b108633f465052c9a Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 13:02:53 +0000 Subject: [PATCH 35/38] test: studios without content releases feature enabled don't do permission checks call (assume permissions) --- .../createReleasePermissionsStore.test.ts | 31 ++++++++++--------- .../store/createReleasePermissionsStore.ts | 4 +++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/sanity/src/core/releases/store/__tests__/createReleasePermissionsStore.test.ts b/packages/sanity/src/core/releases/store/__tests__/createReleasePermissionsStore.test.ts index 49f5475474f..9503ca156d9 100644 --- a/packages/sanity/src/core/releases/store/__tests__/createReleasePermissionsStore.test.ts +++ b/packages/sanity/src/core/releases/store/__tests__/createReleasePermissionsStore.test.ts @@ -1,25 +1,28 @@ -import {beforeEach, describe, expect, it, vi} from 'vitest' +import {describe, expect, it, vi} from 'vitest' import {createReleasePermissionsStore} from '../createReleasePermissionsStore' -import {type useReleasePermissionsValue} from '../useReleasePermissions' - -const createStore = () => createReleasePermissionsStore() describe('useReleasePermissions', () => { - let store: useReleasePermissionsValue + describe('when content release feature is enabled', () => { + it('should return true when action succeeds', async () => { + const mockAction = vi.fn().mockResolvedValueOnce(undefined) + const result = await createReleasePermissionsStore(true).checkWithPermissionGuard(mockAction) - beforeEach(() => { - store = createStore() + expect(result).toBe(true) + expect(mockAction).toHaveBeenCalledWith({ + dryRun: true, + skipCrossDatasetReferenceValidation: true, + }) + }) }) - it('should return true when action succeeds', async () => { - const mockAction = vi.fn().mockResolvedValueOnce(undefined) - const result = await store.checkWithPermissionGuard(mockAction) + describe('when content release feature is disabled', () => { + it('should allow permissions', async () => { + const mockAction = vi.fn() + const result = await createReleasePermissionsStore(false).checkWithPermissionGuard(mockAction) - expect(result).toBe(true) - expect(mockAction).toHaveBeenCalledWith({ - dryRun: true, - skipCrossDatasetReferenceValidation: true, + expect(result).toBe(true) + expect(mockAction).not.toHaveBeenCalled() }) }) }) diff --git a/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts b/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts index a91b4794d3c..0403ea5793e 100644 --- a/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts +++ b/packages/sanity/src/core/releases/store/createReleasePermissionsStore.ts @@ -39,6 +39,10 @@ export function createReleasePermissionsStore( ...args: Parameters ): Promise => { if (!isContentReleasesEnabled) { + /** + * When content releases feature flag is disabled + * assume allowed permissions to provide upsell + */ return true } From 72ab555c16a6953e2f286d7e36cb8c256723482e Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Thu, 20 Feb 2025 15:49:43 +0000 Subject: [PATCH 36/38] refactor: trying to dedupe fetchReleaseLimits calls --- .../contexts/upsell/ReleasesUpsellProvider.tsx | 5 +++-- .../contexts/upsell/fetchReleaseLimits.ts | 13 +++++++------ .../core/releases/hooks/useIsReleasesPlus.ts | 4 +++- .../releases/store/useOrgActiveReleaseCount.ts | 17 ++++++++--------- .../src/core/releases/store/useReleaseLimits.ts | 13 ++++++------- 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx index f18b2893267..6a3ae5c3310 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx +++ b/packages/sanity/src/core/releases/contexts/upsell/ReleasesUpsellProvider.tsx @@ -164,8 +164,9 @@ export function ReleasesUpsellProvider(props: {children: React.ReactNode}) { [telemetry], ) - const {releaseLimits$} = useReleaseLimits() - const {orgActiveReleaseCount$} = useOrgActiveReleaseCount() + const limitsClient = client.withConfig({apiVersion: 'vX'}).observable + const {releaseLimits$} = useReleaseLimits(limitsClient) + const {orgActiveReleaseCount$} = useOrgActiveReleaseCount(limitsClient) const guardWithReleaseLimitUpsell = useCallback( async (cb: () => void, throwError: boolean = false) => { diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index e307c2027a6..5d4b9013560 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -1,5 +1,5 @@ -import {type ClientError, type SanityClient} from '@sanity/client' -import {catchError, map, type Observable, of} from 'rxjs' +import {type ClientError, type ObservableSanityClient} from '@sanity/client' +import {catchError, map, type Observable, of, shareReplay} from 'rxjs' export interface ReleaseLimits { orgActiveReleaseCount: number @@ -17,22 +17,23 @@ interface ReleaseLimitsResponse { * @internal * fetches subscriptions for this project */ -export function fetchReleaseLimits(client: SanityClient): Observable { - const {projectId} = client.config() +export function fetchReleaseLimits(clientOb: ObservableSanityClient): Observable { + const {projectId} = clientOb.config() // This endpoint is prone to optimisation and further work // it will never live within a versions API and will always be on vX // until it goes away - there is graceful handling in `catchError` // for when this endpoint is no longer available and limits are fetched // some other way - const clientX = client.withConfig({apiVersion: 'vX'}) - return clientX.observable + return clientOb .request({ uri: `projects/${projectId}/new-content-release-allowed`, + // tag: `new-${new Date().getTime()}`, tag: 'new-content-release-allowed', }) .pipe( + shareReplay(1), catchError((error: ClientError) => { console.error(error) diff --git a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts index dbdf254fdb5..57f4be99262 100644 --- a/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts +++ b/packages/sanity/src/core/releases/hooks/useIsReleasesPlus.ts @@ -1,5 +1,6 @@ import {useObservable} from 'react-rx' +import {useClient} from '../../hooks/useClient' import {useReleaseLimits} from '../store/useReleaseLimits' /** @@ -7,7 +8,8 @@ import {useReleaseLimits} from '../store/useReleaseLimits' * @returns `boolean` Whether the current org is on a Releases+ plan */ export const useIsReleasesPlus = (): boolean => { - const {releaseLimits$} = useReleaseLimits() + const client = useClient().observable + const {releaseLimits$} = useReleaseLimits(client) const releaseLimit = useObservable(releaseLimits$, null) diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 5859ab66351..602ebb9871d 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -1,8 +1,7 @@ -import {type SanityClient} from '@sanity/client' +import {type ObservableSanityClient} from '@sanity/client' import {useMemo} from 'react' import {BehaviorSubject, catchError, map, type Observable, of, switchMap, tap, timer} from 'rxjs' -import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleaseLimits, type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' import {useActiveReleases} from './useActiveReleases' @@ -16,7 +15,7 @@ const STATE_TTL_MS = 15_000 const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount' function createOrgActiveReleaseCountStore( - client: SanityClient, + client: ObservableSanityClient, activeReleasesCount: number, ): OrgActiveReleaseCountStore { const latestFetchState = new BehaviorSubject(null) @@ -75,10 +74,10 @@ function createOrgActiveReleaseCountStore( * * @returns An Observable of the cached value for org's active release count. */ -export const useOrgActiveReleaseCount = () => { +export const useOrgActiveReleaseCount = (clientOb: ObservableSanityClient) => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() - const client = useClient() + // const client = useClient() const activeReleasesCount = activeReleases?.length || 0 @@ -87,16 +86,16 @@ export const useOrgActiveReleaseCount = () => { return useMemo(() => { const releaseLimitsStore = resourceCache.get({ - dependencies: [client, count], + dependencies: [clientOb, count], namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, - }) || createOrgActiveReleaseCountStore(client, activeReleasesCount) + }) || createOrgActiveReleaseCountStore(clientOb, activeReleasesCount) resourceCache.set({ namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, value: releaseLimitsStore, - dependencies: [client, count], + dependencies: [clientOb, count], }) return releaseLimitsStore - }, [activeReleasesCount, client, count, resourceCache]) + }, [activeReleasesCount, clientOb, count, resourceCache]) } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index e43438b44bb..6321878d409 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -2,7 +2,6 @@ import {type SanityClient} from '@sanity/client' import {useMemo} from 'react' import {catchError, map, type Observable, of, shareReplay} from 'rxjs' -import {useClient} from '../../hooks/useClient' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleaseLimits, type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' @@ -42,23 +41,23 @@ function createReleaseLimitsStore(client: SanityClient): ReleaseLimitsStore { * * @returns An Observable of the cached value for the release limits */ -export const useReleaseLimits: () => ReleaseLimitsStore = () => { +export const useReleaseLimits: (clientOb: any) => ReleaseLimitsStore = (clientOb) => { const resourceCache = useResourceCache() - const client = useClient() + // const client = useClient() return useMemo(() => { const releaseLimitsStore = resourceCache.get({ - dependencies: [client], + dependencies: [clientOb], namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, - }) || createReleaseLimitsStore(client) + }) || createReleaseLimitsStore(clientOb) resourceCache.set({ namespace: RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE, value: releaseLimitsStore, - dependencies: [client], + dependencies: [clientOb], }) return releaseLimitsStore - }, [client, resourceCache]) + }, [clientOb, resourceCache]) } From 3b2a4ebaf076802f763eeebe5539b60595d55839 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 20 Feb 2025 22:28:54 +0000 Subject: [PATCH 37/38] wip: share fetcher observable in order to deduplicate requests --- .../store/useOrgActiveReleaseCount.ts | 20 +++++++++++-------- .../core/releases/store/useReleaseLimits.ts | 5 +++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts index 602ebb9871d..488477b17ca 100644 --- a/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts +++ b/packages/sanity/src/core/releases/store/useOrgActiveReleaseCount.ts @@ -3,8 +3,9 @@ import {useMemo} from 'react' import {BehaviorSubject, catchError, map, type Observable, of, switchMap, tap, timer} from 'rxjs' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' -import {fetchReleaseLimits, type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' +import {type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' import {useActiveReleases} from './useActiveReleases' +import {type ReleaseLimitsStore, useReleaseLimits} from './useReleaseLimits' interface OrgActiveReleaseCountStore { orgActiveReleaseCount$: Observable @@ -15,7 +16,7 @@ const STATE_TTL_MS = 15_000 const ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE = 'orgActiveReleaseCount' function createOrgActiveReleaseCountStore( - client: ObservableSanityClient, + releaseLimits$: ReleaseLimitsStore['releaseLimits$'], activeReleasesCount: number, ): OrgActiveReleaseCountStore { const latestFetchState = new BehaviorSubject(null) @@ -31,9 +32,9 @@ function createOrgActiveReleaseCountStore( ) { staleFlag$.next(false) - return fetchReleaseLimits(client).pipe( + return releaseLimits$.pipe( tap(() => activeReleaseCountAtFetch.next(activeReleasesCount)), - map((data) => data.orgActiveReleaseCount), + map((data) => data?.orgActiveReleaseCount), catchError((error) => { console.error('Failed to fetch org release count', error) @@ -43,14 +44,16 @@ function createOrgActiveReleaseCountStore( return of(state) }), switchMap((nextState) => { - latestFetchState.next(nextState) + if (typeof nextState === 'number') { + latestFetchState.next(nextState) + } timer(STATE_TTL_MS).subscribe(() => { staleFlag$.next(true) activeReleaseCountAtFetch.next(null) }) - return of(nextState) + return of(nextState ?? 0) }), ) } @@ -78,6 +81,7 @@ export const useOrgActiveReleaseCount = (clientOb: ObservableSanityClient) => { const resourceCache = useResourceCache() const {data: activeReleases} = useActiveReleases() // const client = useClient() + const {releaseLimits$} = useReleaseLimits(clientOb) const activeReleasesCount = activeReleases?.length || 0 @@ -88,7 +92,7 @@ export const useOrgActiveReleaseCount = (clientOb: ObservableSanityClient) => { resourceCache.get({ dependencies: [clientOb, count], namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, - }) || createOrgActiveReleaseCountStore(clientOb, activeReleasesCount) + }) || createOrgActiveReleaseCountStore(releaseLimits$, activeReleasesCount) resourceCache.set({ namespace: ORG_ACTIVE_RELEASE_COUNT_RESOURCE_CACHE_NAMESPACE, @@ -97,5 +101,5 @@ export const useOrgActiveReleaseCount = (clientOb: ObservableSanityClient) => { }) return releaseLimitsStore - }, [activeReleasesCount, clientOb, count, resourceCache]) + }, [activeReleasesCount, clientOb, count, releaseLimits$, resourceCache]) } diff --git a/packages/sanity/src/core/releases/store/useReleaseLimits.ts b/packages/sanity/src/core/releases/store/useReleaseLimits.ts index 6321878d409..5e5cb7c16f5 100644 --- a/packages/sanity/src/core/releases/store/useReleaseLimits.ts +++ b/packages/sanity/src/core/releases/store/useReleaseLimits.ts @@ -5,8 +5,8 @@ import {catchError, map, type Observable, of, shareReplay} from 'rxjs' import {useResourceCache} from '../../store/_legacy/ResourceCacheProvider' import {fetchReleaseLimits, type ReleaseLimits} from '../contexts/upsell/fetchReleaseLimits' -interface ReleaseLimitsStore { - releaseLimits$: Observable | null> +export interface ReleaseLimitsStore { + releaseLimits$: Observable } const RELEASE_LIMITS_RESOURCE_CACHE_NAMESPACE = 'ReleaseLimits' @@ -17,6 +17,7 @@ function createReleaseLimitsStore(client: SanityClient): ReleaseLimitsStore { defaultOrgActiveReleaseLimit: data.defaultOrgActiveReleaseLimit, datasetReleaseLimit: data.datasetReleaseLimit, orgActiveReleaseLimit: data.orgActiveReleaseLimit, + orgActiveReleaseCount: data.orgActiveReleaseCount, })), shareReplay(1), catchError((error) => { From e58b2978c46f881093262b1bbb38f3bc938a300d Mon Sep 17 00:00:00 2001 From: Jordan Lawrence Date: Fri, 21 Feb 2025 11:54:44 +0000 Subject: [PATCH 38/38] fix: removed shareReplay from fetch limits and using vX --- .../src/core/releases/contexts/upsell/fetchReleaseLimits.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts index 5d4b9013560..32dd5a9ace5 100644 --- a/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts +++ b/packages/sanity/src/core/releases/contexts/upsell/fetchReleaseLimits.ts @@ -1,5 +1,5 @@ import {type ClientError, type ObservableSanityClient} from '@sanity/client' -import {catchError, map, type Observable, of, shareReplay} from 'rxjs' +import {catchError, map, type Observable, of, share} from 'rxjs' export interface ReleaseLimits { orgActiveReleaseCount: number @@ -27,13 +27,14 @@ export function fetchReleaseLimits(clientOb: ObservableSanityClient): Observable // some other way return clientOb + .withConfig({apiVersion: 'vX'}) .request({ uri: `projects/${projectId}/new-content-release-allowed`, // tag: `new-${new Date().getTime()}`, tag: 'new-content-release-allowed', }) .pipe( - shareReplay(1), + share(), catchError((error: ClientError) => { console.error(error)