Skip to content

Commit

Permalink
refactor(preview): extract global listener, refactor preview APIs and…
Browse files Browse the repository at this point in the history
… improve typings
  • Loading branch information
bjoerge committed Aug 12, 2024
1 parent 12d1411 commit 1ae5e37
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 201 deletions.
50 changes: 23 additions & 27 deletions packages/sanity/src/core/preview/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,34 +67,10 @@ function mutConcat<T>(array: T[], chunks: T[]) {
return array
}

export function create_preview_availability(
export function createPreviewAvailabilityObserver(
versionedClient: SanityClient,
observePaths: ObservePathsFn,
): {
observeDocumentPairAvailability(id: string): Observable<DraftsModelDocumentAvailability>
} {
/**
* Returns an observable of metadata for a given drafts model document
*/
function observeDocumentPairAvailability(
id: string,
): Observable<DraftsModelDocumentAvailability> {
const draftId = getDraftId(id)
const publishedId = getPublishedId(id)
return combineLatest([
observeDocumentAvailability(draftId),
observeDocumentAvailability(publishedId),
]).pipe(
distinctUntilChanged(shallowEquals),
map(([draftReadability, publishedReadability]) => {
return {
draft: draftReadability,
published: publishedReadability,
}
}),
)
}

): (id: string) => Observable<DraftsModelDocumentAvailability> {
/**
* Observable of metadata for the document with the given id
* If we can't read a document it is either because it's not readable or because it doesn't exist
Expand Down Expand Up @@ -158,5 +134,25 @@ export function create_preview_availability(
})
}

return {observeDocumentPairAvailability}
/**
* Returns an observable of metadata for a given drafts model document
*/
return function observeDocumentPairAvailability(
id: string,
): Observable<DraftsModelDocumentAvailability> {
const draftId = getDraftId(id)
const publishedId = getPublishedId(id)
return combineLatest([
observeDocumentAvailability(draftId),
observeDocumentAvailability(publishedId),
]).pipe(
distinctUntilChanged(shallowEquals),
map(([draftReadability, publishedReadability]) => {
return {
draft: draftReadability,
published: publishedReadability,
}
}),
)
}
}
6 changes: 6 additions & 0 deletions packages/sanity/src/core/preview/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import {type PreviewValue} from '@sanity/types'
export const INCLUDE_FIELDS_QUERY = ['_id', '_rev', '_type']
export const INCLUDE_FIELDS = [...INCLUDE_FIELDS_QUERY, '_key']

/**
* How long to wait after the last subscriber has unsubscribed before resetting the observable and disconnecting the listener
* We want to keep the listener alive for a short while after the last subscriber has unsubscribed to avoid unnecessary reconnects
*/
export const LISTENER_RESET_DELAY = 2000

export const AVAILABILITY_READABLE = {
available: true,
reason: 'READABLE',
Expand Down
31 changes: 31 additions & 0 deletions packages/sanity/src/core/preview/createGlobalListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {type SanityClient} from '@sanity/client'
import {timer} from 'rxjs'

import {LISTENER_RESET_DELAY} from './constants'
import {shareReplayLatest} from './utils/shareReplayLatest'

/**
* @internal
* Creates a listener that will emit 'welcome' for all new subscribers immediately, and thereafter emit at every mutation event
*/
export function createGlobalListener(client: SanityClient) {
return client
.listen(
'*[!(_id in path("_.**"))]',
{},
{
events: ['welcome', 'mutation', 'reconnect'],
includeResult: false,
includePreviousRevision: false,
includeMutations: false,
visibility: 'query',
tag: 'preview.global',
},
)
.pipe(
shareReplayLatest({
predicate: (event) => event.type === 'welcome' || event.type === 'reconnect',
resetOnRefCountZero: () => timer(LISTENER_RESET_DELAY),
}),
)
}
25 changes: 14 additions & 11 deletions packages/sanity/src/core/preview/createPathObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,19 @@ function normalizePaths(path: (FieldName | PreviewPath)[]): PreviewPath[] {
)
}

export function createPathObserver(context: {observeFields: ObserveFieldsFn}) {
const {observeFields} = context

return {
observePaths(
value: Previewable,
paths: (FieldName | PreviewPath)[],
apiConfig?: ApiConfig,
): Observable<Record<string, unknown> | null> {
return observePaths(value, normalizePaths(paths), observeFields, apiConfig)
},
/**
* Creates a function that allows observing nested paths on a document.
* If the path includes a reference, the reference will be "followed", allowing for selecting paths within the referenced document.
* @param options - Options - Requires a function that can observe fields on a document
* */
export function createPathObserver(options: {observeFields: ObserveFieldsFn}) {
const {observeFields} = options

return (
value: Previewable,
paths: (FieldName | PreviewPath)[],
apiConfig?: ApiConfig,
): Observable<Record<string, unknown> | null> => {
return observePaths(value, normalizePaths(paths), observeFields, apiConfig)
}
}
46 changes: 25 additions & 21 deletions packages/sanity/src/core/preview/createPreviewObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import {
type PrepareViewOptions,
} from '@sanity/types'
import {isPlainObject} from 'lodash'
import {type Observable, of as observableOf} from 'rxjs'
import {type Observable, of} from 'rxjs'
import {map, switchMap} from 'rxjs/operators'

import {type ObserveForPreviewFn} from './documentPreviewStore'
import {
type ApiConfig,
type ObserveDocumentTypeFromIdFn,
type ObservePathsFn,
type PreparedSnapshot,
type Previewable,
type PreviewableType,
type PreviewPath,
} from './types'
import {getPreviewPaths} from './utils/getPreviewPaths'
import {invokePrepare, prepareForPreview} from './utils/prepareForPreview'
Expand All @@ -28,27 +30,25 @@ function isReference(value: unknown): value is {_ref: string} {

// Takes a value and its type and prepares a snapshot for it that can be passed to a preview component
export function createPreviewObserver(context: {
observeDocumentTypeFromId: (id: string, apiConfig?: ApiConfig) => Observable<string | undefined>
observePaths: (value: Previewable, paths: PreviewPath[], apiConfig?: ApiConfig) => any
}): (
value: Previewable,
type: PreviewableType,
viewOptions?: PrepareViewOptions,
apiConfig?: ApiConfig,
) => Observable<PreparedSnapshot> {
observeDocumentTypeFromId: ObserveDocumentTypeFromIdFn
observePaths: ObservePathsFn
}): ObserveForPreviewFn {
const {observeDocumentTypeFromId, observePaths} = context

return function observeForPreview(
value: Previewable,
type: PreviewableType,
viewOptions?: PrepareViewOptions,
apiConfig?: ApiConfig,
options: {
viewOptions?: PrepareViewOptions
apiConfig?: ApiConfig
} = {},
): Observable<PreparedSnapshot> {
const {viewOptions = {}, apiConfig} = options
if (isCrossDatasetReferenceSchemaType(type)) {
// if the value is of type crossDatasetReference, but has no _ref property, we cannot prepare any value for the preview
// and the most appropriate thing to do is to return `undefined` for snapshot
if (!isCrossDatasetReference(value)) {
return observableOf({snapshot: undefined})
return of({snapshot: undefined})
}

const refApiConfig = {projectId: value._projectId, dataset: value._dataset}
Expand All @@ -57,32 +57,36 @@ export function createPreviewObserver(context: {
switchMap((typeName) => {
if (typeName) {
const refType = type.to.find((toType) => toType.type === typeName)
return observeForPreview(value, refType as any, {}, refApiConfig)
if (refType) {
return observeForPreview(value, refType, {apiConfig: refApiConfig, viewOptions})
}
}
return observableOf({snapshot: undefined})
return of({snapshot: undefined})
}),
)
}
if (isReferenceSchemaType(type)) {
// if the value is of type reference, but has no _ref property, we cannot prepare any value for the preview
// and the most appropriate thing to do is to return `undefined` for snapshot
if (!isReference(value)) {
return observableOf({snapshot: undefined})
return of({snapshot: undefined})
}
// Previewing references actually means getting the referenced value,
// and preview using the preview config of its type
// todo: We need a way of knowing the type of the referenced value by looking at the reference record alone
// We do this since there's no way of knowing the type of the referenced value by looking at the reference value alone
return observeDocumentTypeFromId(value._ref).pipe(
switchMap((typeName) => {
if (typeName) {
const refType = type.to.find((toType) => toType.name === typeName)
return observeForPreview(value, refType as any)
if (refType) {
return observeForPreview(value, refType)
}
}
// todo: in case we can't read the document type, we can figure out the reason why e.g. whether it's because
// the document doesn't exist or it's not readable due to lack of permission.
// We can use the "observeDocumentAvailability" function
// for this, but currently not sure if needed
return observableOf({snapshot: undefined})
return of({snapshot: undefined})
}),
)
}
Expand All @@ -91,7 +95,7 @@ export function createPreviewObserver(context: {
return observePaths(value, paths, apiConfig).pipe(
map((snapshot) => ({
type: type,
snapshot: snapshot && prepareForPreview(snapshot, type as any, viewOptions),
snapshot: snapshot ? prepareForPreview(snapshot, type, viewOptions) : null,
})),
)
}
Expand All @@ -100,7 +104,7 @@ export function createPreviewObserver(context: {
// the SchemaType doesn't have a `select` field. The schema compiler
// provides a default `preview` implementation for `object`s, `image`s,
// `file`s, and `document`s
return observableOf({
return of({
type,
snapshot:
value && isRecord(value) ? invokePrepare(type, value, viewOptions).returnValue : null,
Expand Down
34 changes: 15 additions & 19 deletions packages/sanity/src/core/preview/documentPair.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
import {type SanityClient} from '@sanity/client'
import {type SanityDocument} from '@sanity/types'
import {combineLatest, type Observable, of} from 'rxjs'
import {map, switchMap} from 'rxjs/operators'

import {getIdPair, isRecord} from '../util'
import {create_preview_availability} from './availability'
import {type DraftsModelDocument, type ObservePathsFn, type PreviewPath} from './types'
import {
type DraftsModelDocument,
type ObserveDocumentAvailabilityFn,
type ObservePathsFn,
type PreviewPath,
} from './types'

export function create_preview_documentPair(
versionedClient: SanityClient,
observePaths: ObservePathsFn,
): {
observePathsDocumentPair: <T extends SanityDocument = SanityDocument>(
id: string,
paths: PreviewPath[],
) => Observable<DraftsModelDocument<T>>
} {
const {observeDocumentPairAvailability} = create_preview_availability(
versionedClient,
observePaths,
)
export function createObservePathsDocumentPair(options: {
observeDocumentPairAvailability: ObserveDocumentAvailabilityFn
observePaths: ObservePathsFn
}): <T extends SanityDocument = SanityDocument>(
id: string,
paths: PreviewPath[],
) => Observable<DraftsModelDocument<T>> {
const {observeDocumentPairAvailability, observePaths} = options

const ALWAYS_INCLUDED_SNAPSHOT_PATHS: PreviewPath[] = [['_updatedAt'], ['_createdAt'], ['_type']]

return {observePathsDocumentPair}

function observePathsDocumentPair<T extends SanityDocument = SanityDocument>(
return function observePathsDocumentPair<T extends SanityDocument = SanityDocument>(
id: string,
paths: PreviewPath[],
): Observable<DraftsModelDocument<T>> {
Expand Down
Loading

0 comments on commit 1ae5e37

Please sign in to comment.