Skip to content

Commit 5c75a38

Browse files
bjoergerexxars
andauthored
refactor(preview): extract global listener, refactor preview APIs and improve typings (#7360)
* refactor(preview): extract global listener, refactor preview APIs and improve typings * fixup! refactor(preview): extract global listener, refactor preview APIs and improve typings * Apply suggestions from code review Co-authored-by: Espen Hovlandsdal <[email protected]> --------- Co-authored-by: Espen Hovlandsdal <[email protected]>
1 parent f273249 commit 5c75a38

15 files changed

+335
-202
lines changed

packages/sanity/src/core/preview/availability.ts

+23-27
Original file line numberDiff line numberDiff line change
@@ -67,34 +67,10 @@ function mutConcat<T>(array: T[], chunks: T[]) {
6767
return array
6868
}
6969

70-
export function create_preview_availability(
70+
export function createPreviewAvailabilityObserver(
7171
versionedClient: SanityClient,
7272
observePaths: ObservePathsFn,
73-
): {
74-
observeDocumentPairAvailability(id: string): Observable<DraftsModelDocumentAvailability>
75-
} {
76-
/**
77-
* Returns an observable of metadata for a given drafts model document
78-
*/
79-
function observeDocumentPairAvailability(
80-
id: string,
81-
): Observable<DraftsModelDocumentAvailability> {
82-
const draftId = getDraftId(id)
83-
const publishedId = getPublishedId(id)
84-
return combineLatest([
85-
observeDocumentAvailability(draftId),
86-
observeDocumentAvailability(publishedId),
87-
]).pipe(
88-
distinctUntilChanged(shallowEquals),
89-
map(([draftReadability, publishedReadability]) => {
90-
return {
91-
draft: draftReadability,
92-
published: publishedReadability,
93-
}
94-
}),
95-
)
96-
}
97-
73+
): (id: string) => Observable<DraftsModelDocumentAvailability> {
9874
/**
9975
* Observable of metadata for the document with the given id
10076
* If we can't read a document it is either because it's not readable or because it doesn't exist
@@ -158,5 +134,25 @@ export function create_preview_availability(
158134
})
159135
}
160136

161-
return {observeDocumentPairAvailability}
137+
/**
138+
* Returns an observable of metadata for a given drafts model document
139+
*/
140+
return function observeDocumentPairAvailability(
141+
id: string,
142+
): Observable<DraftsModelDocumentAvailability> {
143+
const draftId = getDraftId(id)
144+
const publishedId = getPublishedId(id)
145+
return combineLatest([
146+
observeDocumentAvailability(draftId),
147+
observeDocumentAvailability(publishedId),
148+
]).pipe(
149+
distinctUntilChanged(shallowEquals),
150+
map(([draftReadability, publishedReadability]) => {
151+
return {
152+
draft: draftReadability,
153+
published: publishedReadability,
154+
}
155+
}),
156+
)
157+
}
162158
}

packages/sanity/src/core/preview/constants.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import {type PreviewValue} from '@sanity/types'
44
export const INCLUDE_FIELDS_QUERY = ['_id', '_rev', '_type']
55
export const INCLUDE_FIELDS = [...INCLUDE_FIELDS_QUERY, '_key']
66

7+
/**
8+
* How long to wait after the last subscriber has unsubscribed before resetting the observable and disconnecting the listener
9+
* We want to keep the listener alive for a short while after the last subscriber has unsubscribed to avoid unnecessary reconnects
10+
*/
11+
export const LISTENER_RESET_DELAY = 10_000
12+
713
export const AVAILABILITY_READABLE = {
814
available: true,
915
reason: 'READABLE',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {type SanityClient} from '@sanity/client'
2+
import {timer} from 'rxjs'
3+
4+
import {LISTENER_RESET_DELAY} from './constants'
5+
import {shareReplayLatest} from './utils/shareReplayLatest'
6+
7+
/**
8+
* @internal
9+
* Creates a listener that will emit 'welcome' for all new subscribers immediately, and thereafter emit at every mutation event
10+
*/
11+
export function createGlobalListener(client: SanityClient) {
12+
return client
13+
.listen(
14+
'*[!(_id in path("_.**"))]',
15+
{},
16+
{
17+
events: ['welcome', 'mutation', 'reconnect'],
18+
includeResult: false,
19+
includePreviousRevision: false,
20+
includeMutations: false,
21+
visibility: 'query',
22+
tag: 'preview.global',
23+
},
24+
)
25+
.pipe(
26+
shareReplayLatest({
27+
predicate: (event) => event.type === 'welcome' || event.type === 'reconnect',
28+
resetOnRefCountZero: () => timer(LISTENER_RESET_DELAY),
29+
}),
30+
)
31+
}

packages/sanity/src/core/preview/createPathObserver.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,20 @@ function normalizePaths(path: (FieldName | PreviewPath)[]): PreviewPath[] {
119119
)
120120
}
121121

122-
export function createPathObserver(context: {observeFields: ObserveFieldsFn}) {
123-
const {observeFields} = context
124-
125-
return {
126-
observePaths(
127-
value: Previewable,
128-
paths: (FieldName | PreviewPath)[],
129-
apiConfig?: ApiConfig,
130-
): Observable<Record<string, unknown> | null> {
131-
return observePaths(value, normalizePaths(paths), observeFields, apiConfig)
132-
},
122+
/**
123+
* Creates a function that allows observing nested paths on a document.
124+
* If the path includes a reference, the reference will be "followed", allowing for selecting paths within the referenced document.
125+
* @param options - Requires a function that can observe fields on a document
126+
* @internal
127+
*/
128+
export function createPathObserver(options: {observeFields: ObserveFieldsFn}) {
129+
const {observeFields} = options
130+
131+
return (
132+
value: Previewable,
133+
paths: (FieldName | PreviewPath)[],
134+
apiConfig?: ApiConfig,
135+
): Observable<Record<string, unknown> | null> => {
136+
return observePaths(value, normalizePaths(paths), observeFields, apiConfig)
133137
}
134138
}

packages/sanity/src/core/preview/createPreviewObserver.ts

+29-22
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import {
55
type PrepareViewOptions,
66
} from '@sanity/types'
77
import {isPlainObject} from 'lodash'
8-
import {type Observable, of as observableOf} from 'rxjs'
8+
import {type Observable, of} from 'rxjs'
99
import {map, switchMap} from 'rxjs/operators'
1010

11+
import {type ObserveForPreviewFn} from './documentPreviewStore'
1112
import {
1213
type ApiConfig,
14+
type ObserveDocumentTypeFromIdFn,
15+
type ObservePathsFn,
1316
type PreparedSnapshot,
1417
type Previewable,
1518
type PreviewableType,
16-
type PreviewPath,
1719
} from './types'
1820
import {getPreviewPaths} from './utils/getPreviewPaths'
1921
import {invokePrepare, prepareForPreview} from './utils/prepareForPreview'
@@ -26,29 +28,30 @@ function isReference(value: unknown): value is {_ref: string} {
2628
return isPlainObject(value)
2729
}
2830

29-
// Takes a value and its type and prepares a snapshot for it that can be passed to a preview component
31+
/**
32+
* Takes a value and its type and prepares a snapshot for it that can be passed to a preview component
33+
* @internal
34+
*/
3035
export function createPreviewObserver(context: {
31-
observeDocumentTypeFromId: (id: string, apiConfig?: ApiConfig) => Observable<string | undefined>
32-
observePaths: (value: Previewable, paths: PreviewPath[], apiConfig?: ApiConfig) => any
33-
}): (
34-
value: Previewable,
35-
type: PreviewableType,
36-
viewOptions?: PrepareViewOptions,
37-
apiConfig?: ApiConfig,
38-
) => Observable<PreparedSnapshot> {
36+
observeDocumentTypeFromId: ObserveDocumentTypeFromIdFn
37+
observePaths: ObservePathsFn
38+
}): ObserveForPreviewFn {
3939
const {observeDocumentTypeFromId, observePaths} = context
4040

4141
return function observeForPreview(
4242
value: Previewable,
4343
type: PreviewableType,
44-
viewOptions?: PrepareViewOptions,
45-
apiConfig?: ApiConfig,
44+
options: {
45+
viewOptions?: PrepareViewOptions
46+
apiConfig?: ApiConfig
47+
} = {},
4648
): Observable<PreparedSnapshot> {
49+
const {viewOptions = {}, apiConfig} = options
4750
if (isCrossDatasetReferenceSchemaType(type)) {
4851
// if the value is of type crossDatasetReference, but has no _ref property, we cannot prepare any value for the preview
4952
// and the most appropriate thing to do is to return `undefined` for snapshot
5053
if (!isCrossDatasetReference(value)) {
51-
return observableOf({snapshot: undefined})
54+
return of({snapshot: undefined})
5255
}
5356

5457
const refApiConfig = {projectId: value._projectId, dataset: value._dataset}
@@ -57,32 +60,36 @@ export function createPreviewObserver(context: {
5760
switchMap((typeName) => {
5861
if (typeName) {
5962
const refType = type.to.find((toType) => toType.type === typeName)
60-
return observeForPreview(value, refType as any, {}, refApiConfig)
63+
if (refType) {
64+
return observeForPreview(value, refType, {apiConfig: refApiConfig, viewOptions})
65+
}
6166
}
62-
return observableOf({snapshot: undefined})
67+
return of({snapshot: undefined})
6368
}),
6469
)
6570
}
6671
if (isReferenceSchemaType(type)) {
6772
// if the value is of type reference, but has no _ref property, we cannot prepare any value for the preview
6873
// and the most appropriate thing to do is to return `undefined` for snapshot
6974
if (!isReference(value)) {
70-
return observableOf({snapshot: undefined})
75+
return of({snapshot: undefined})
7176
}
7277
// Previewing references actually means getting the referenced value,
7378
// and preview using the preview config of its type
74-
// todo: We need a way of knowing the type of the referenced value by looking at the reference record alone
79+
// We do this since there's no way of knowing the type of the referenced value by looking at the reference value alone
7580
return observeDocumentTypeFromId(value._ref).pipe(
7681
switchMap((typeName) => {
7782
if (typeName) {
7883
const refType = type.to.find((toType) => toType.name === typeName)
79-
return observeForPreview(value, refType as any)
84+
if (refType) {
85+
return observeForPreview(value, refType)
86+
}
8087
}
8188
// todo: in case we can't read the document type, we can figure out the reason why e.g. whether it's because
8289
// the document doesn't exist or it's not readable due to lack of permission.
8390
// We can use the "observeDocumentAvailability" function
8491
// for this, but currently not sure if needed
85-
return observableOf({snapshot: undefined})
92+
return of({snapshot: undefined})
8693
}),
8794
)
8895
}
@@ -91,7 +98,7 @@ export function createPreviewObserver(context: {
9198
return observePaths(value, paths, apiConfig).pipe(
9299
map((snapshot) => ({
93100
type: type,
94-
snapshot: snapshot && prepareForPreview(snapshot, type as any, viewOptions),
101+
snapshot: snapshot ? prepareForPreview(snapshot, type, viewOptions) : null,
95102
})),
96103
)
97104
}
@@ -100,7 +107,7 @@ export function createPreviewObserver(context: {
100107
// the SchemaType doesn't have a `select` field. The schema compiler
101108
// provides a default `preview` implementation for `object`s, `image`s,
102109
// `file`s, and `document`s
103-
return observableOf({
110+
return of({
104111
type,
105112
snapshot:
106113
value && isRecord(value) ? invokePrepare(type, value, viewOptions).returnValue : null,

packages/sanity/src/core/preview/documentPair.ts

+15-19
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
1-
import {type SanityClient} from '@sanity/client'
21
import {type SanityDocument} from '@sanity/types'
32
import {combineLatest, type Observable, of} from 'rxjs'
43
import {map, switchMap} from 'rxjs/operators'
54

65
import {getIdPair, isRecord} from '../util'
7-
import {create_preview_availability} from './availability'
8-
import {type DraftsModelDocument, type ObservePathsFn, type PreviewPath} from './types'
6+
import {
7+
type DraftsModelDocument,
8+
type ObserveDocumentAvailabilityFn,
9+
type ObservePathsFn,
10+
type PreviewPath,
11+
} from './types'
912

10-
export function create_preview_documentPair(
11-
versionedClient: SanityClient,
12-
observePaths: ObservePathsFn,
13-
): {
14-
observePathsDocumentPair: <T extends SanityDocument = SanityDocument>(
15-
id: string,
16-
paths: PreviewPath[],
17-
) => Observable<DraftsModelDocument<T>>
18-
} {
19-
const {observeDocumentPairAvailability} = create_preview_availability(
20-
versionedClient,
21-
observePaths,
22-
)
13+
export function createObservePathsDocumentPair(options: {
14+
observeDocumentPairAvailability: ObserveDocumentAvailabilityFn
15+
observePaths: ObservePathsFn
16+
}): <T extends SanityDocument = SanityDocument>(
17+
id: string,
18+
paths: PreviewPath[],
19+
) => Observable<DraftsModelDocument<T>> {
20+
const {observeDocumentPairAvailability, observePaths} = options
2321

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

26-
return {observePathsDocumentPair}
27-
28-
function observePathsDocumentPair<T extends SanityDocument = SanityDocument>(
24+
return function observePathsDocumentPair<T extends SanityDocument = SanityDocument>(
2925
id: string,
3026
paths: PreviewPath[],
3127
): Observable<DraftsModelDocument<T>> {

0 commit comments

Comments
 (0)