Skip to content

Commit

Permalink
feat(sanity): add release layering foundations
Browse files Browse the repository at this point in the history
  • Loading branch information
juice49 committed Oct 24, 2024
1 parent 9dbac15 commit 16a2f63
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 107 deletions.
73 changes: 61 additions & 12 deletions packages/sanity/src/core/preview/utils/getPreviewStateObservable.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import {type PreviewValue, type SanityDocument, type SchemaType} from '@sanity/types'
import {omit} from 'lodash'
import {type ReactNode} from 'react'
import {combineLatest, type Observable, of} from 'rxjs'
import {map, startWith} from 'rxjs/operators'
import {combineLatest, from, type Observable, of} from 'rxjs'
import {map, mergeMap, scan, startWith} from 'rxjs/operators'
import {type PreparedSnapshot} from 'sanity'

import {getDraftId, getPublishedId, getVersionId} from '../../util/draftUtils'
import {type DocumentPreviewStore} from '../documentPreviewStore'

/**
* @internal
*/
export type VersionsRecord = Record<string, PreparedSnapshot>

type VersionTuple = [bundleId: string, snapshot: PreparedSnapshot]

export interface PreviewState {
isLoading?: boolean
draft?: PreviewValue | Partial<SanityDocument> | null
published?: PreviewValue | Partial<SanityDocument> | null
version?: PreviewValue | Partial<SanityDocument> | null
versions: VersionsRecord
}

const isLiveEditEnabled = (schemaType: SchemaType) => schemaType.liveEdit === true
Expand All @@ -25,31 +35,70 @@ export function getPreviewStateObservable(
schemaType: SchemaType,
documentId: string,
title: ReactNode,
perspective?: string,
perspective: {
bundleIds: string[]
bundleStack: string[]
} = {
bundleIds: [],
bundleStack: [],
},
): Observable<PreviewState> {
const draft$ = isLiveEditEnabled(schemaType)
? of({snapshot: null})
: documentPreviewStore.observeForPreview({_id: getDraftId(documentId)}, schemaType)

const version$ = perspective
? documentPreviewStore.observeForPreview(
{_id: getVersionId(documentId, perspective)},
schemaType,
)
: of({snapshot: null})
const versions$ = from(perspective.bundleIds).pipe(
mergeMap<string, Observable<VersionTuple>>((bundleId) =>
documentPreviewStore
.observeForPreview({_id: getVersionId(documentId, bundleId)}, schemaType)
.pipe(map((storeValue) => [bundleId, storeValue])),
),
scan<VersionTuple, VersionsRecord>((byBundleId, [bundleId, value]) => {
if (value.snapshot === null) {
return omit({...byBundleId}, [bundleId])
}

return {
...byBundleId,
[bundleId]: value,
}
}, {}),
startWith<VersionsRecord>({}),
)

// Iterate the release stack in descending precedence, returning the highest precedence existing
// version document.
const version$ = versions$.pipe(
map((versions) => {
for (const bundleId of perspective.bundleStack) {
if (bundleId in versions) {
return versions[bundleId]
}
}
return {snapshot: null}
}),
startWith<PreparedSnapshot>({snapshot: null}),
)

const published$ = documentPreviewStore.observeForPreview(
{_id: getPublishedId(documentId)},
schemaType,
)

return combineLatest([draft$, published$, version$]).pipe(
map(([draft, published, version]) => ({
return combineLatest([draft$, published$, version$, versions$]).pipe(
map(([draft, published, version, versions]) => ({
draft: draft.snapshot ? {title, ...(draft.snapshot || {})} : null,
isLoading: false,
published: published.snapshot ? {title, ...(published.snapshot || {})} : null,
version: version.snapshot ? {title, ...(version.snapshot || {})} : null,
versions,
})),
startWith({draft: null, isLoading: true, published: null, version: null}),
startWith({
draft: null,
isLoading: true,
published: null,
version: null,
versions: {},
}),
)
}
10 changes: 7 additions & 3 deletions packages/sanity/src/core/store/_legacy/datastores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useTelemetry} from '@sanity/telemetry/react'
import {useCallback, useMemo} from 'react'
import {of} from 'rxjs'

import {useRouter} from '../../../router'
import {useClient, useSchema, useTemplates} from '../../hooks'
import {createDocumentPreviewStore, type DocumentPreviewStore} from '../../preview'
import {useAddonDataset, useSource, useWorkspace} from '../../studio'
Expand Down Expand Up @@ -299,26 +300,29 @@ export function useBundlesStore(): BundlesStore {
const currentUser = useCurrentUser()
const addonDataset = useAddonDataset()
const studioClient = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
const router = useRouter()

// TODO: Include hidden layers state.
return useMemo(() => {
const bundlesStore =
resourceCache.get<BundlesStore>({
dependencies: [workspace, addonDataset, currentUser],
dependencies: [workspace, addonDataset, currentUser, router.perspectiveState],
namespace: 'BundlesStore',
}) ||
createBundlesStore({
addonClient: addonDataset.client,
addonClientReady: addonDataset.ready,
studioClient,
currentUser,
perspective: router.perspectiveState.perspective,
})

resourceCache.set({
dependencies: [workspace, addonDataset, currentUser],
dependencies: [workspace, addonDataset, currentUser, router.perspectiveState],
namespace: 'BundlesStore',
value: bundlesStore,
})

return bundlesStore
}, [resourceCache, workspace, addonDataset, studioClient, currentUser])
}, [resourceCache, workspace, addonDataset, currentUser, router.perspectiveState, studioClient])
}
5 changes: 4 additions & 1 deletion packages/sanity/src/core/store/bundles/createBundlesStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const INITIAL_STATE: bundlesReducerState = {
bundles: new Map(),
deletedBundles: {},
state: 'initialising',
releaseStack: [],
}

const NOOP_BUNDLE_STORE: BundlesStore = {
Expand All @@ -69,6 +70,7 @@ const LOADED_BUNDLE_STORE: BundlesStore = {
bundles: new Map(),
deletedBundles: {},
state: 'loaded' as const,
releaseStack: [],
}),
),
getMetadataStateForSlugs$: () => of({data: null, error: null, loading: false}),
Expand All @@ -88,6 +90,7 @@ export function createBundlesStore(context: {
studioClient: SanityClient | null
addonClientReady: boolean
currentUser: User | null
perspective?: string
}): BundlesStore {
const {addonClient, studioClient, addonClientReady, currentUser} = context

Expand Down Expand Up @@ -251,7 +254,7 @@ export function createBundlesStore(context: {

const state$ = merge(listFetch$, listener$, dispatch$).pipe(
filter((action): action is bundlesReducerAction => typeof action !== 'undefined'),
scan((state, action) => bundlesReducer(state, action), INITIAL_STATE),
scan((state, action) => bundlesReducer(state, action, context.perspective), INITIAL_STATE),
startWith(INITIAL_STATE),
shareReplay(1),
)
Expand Down
57 changes: 57 additions & 0 deletions packages/sanity/src/core/store/bundles/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {DRAFTS_FOLDER, resolveBundlePerspective} from 'sanity'

import {type BundleDocument} from './types'

interface BundleDeletedAction {
Expand Down Expand Up @@ -42,6 +44,12 @@ export interface bundlesReducerState {
deletedBundles: Record<string, BundleDocument>
state: 'initialising' | 'loading' | 'loaded' | 'error'
error?: Error

/**
* An array of release ids ordered chronologically to represent the state of documents at the
* given point in time.
*/
releaseStack: string[]
}

function createBundlesSet(bundles: BundleDocument[] | null) {
Expand All @@ -54,6 +62,7 @@ function createBundlesSet(bundles: BundleDocument[] | null) {
export function bundlesReducer(
state: bundlesReducerState,
action: bundlesReducerAction,
perspective?: string,
): bundlesReducerState {
switch (action.type) {
case 'LOADING_STATE_CHANGED': {
Expand All @@ -71,6 +80,10 @@ export function bundlesReducer(
return {
...state,
bundles: bundlesById,
releaseStack: getReleaseStack({
bundles: bundlesById,
perspective,
}),
}
}

Expand All @@ -82,6 +95,10 @@ export function bundlesReducer(
return {
...state,
bundles: currentBundles,
releaseStack: getReleaseStack({
bundles: currentBundles,
perspective,
}),
}
}

Expand All @@ -105,6 +122,7 @@ export function bundlesReducer(
...state,
bundles: currentBundles,
deletedBundles: nextDeletedBundles,
releaseStack: [...state.releaseStack].filter((id) => id !== deletedBundleId),
}
}

Expand All @@ -117,10 +135,49 @@ export function bundlesReducer(
return {
...state,
bundles: currentBundles,
releaseStack: getReleaseStack({
bundles: currentBundles,
perspective,
}),
}
}

default:
return state
}
}

function getReleaseStack({
bundles,
perspective,
}: {
bundles?: Map<string, BundleDocument>
perspective?: string
}): string[] {
if (typeof bundles === 'undefined') {
return []
}

// TODO: Handle system perspectives.
if (!perspective?.startsWith('bundle.')) {
return []
}

const stack = [...bundles.values()]
.toSorted(sortReleases(resolveBundlePerspective(perspective)))
.map(({_id}) => _id)
.concat(DRAFTS_FOLDER)

return stack
}

// TODO: Implement complete layering heuristics.
function sortReleases(perspective?: string): (a: BundleDocument, b: BundleDocument) => number {
return function (a, b) {
// Ensure the current release takes highest precedence.
if (a._id === perspective) {
return -1
}
return 0
}
}
9 changes: 9 additions & 0 deletions packages/sanity/src/core/store/bundles/useBundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import {type BundleDocument} from './types'

interface BundlesState {
data: BundleDocument[] | null
bundles: Map<string, BundleDocument>
deletedBundles: Record<string, BundleDocument>
error?: Error
loading: boolean
dispatch: React.Dispatch<bundlesReducerAction>

/**
* An array of release ids ordered chronologically to represent the state of documents at the
* given point in time.
*/
stack: string[]
}

/**
Expand All @@ -24,9 +31,11 @@ export function useBundles(): BundlesState {

return {
data: bundlesAsArray,
bundles: state.bundles,
deletedBundles,
dispatch,
error,
loading: ['loading', 'initialising'].includes(state.state),
stack: state.releaseStack,
}
}
Loading

0 comments on commit 16a2f63

Please sign in to comment.