-
Notifications
You must be signed in to change notification settings - Fork 448
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(preview): add experimental support for live document id sets (#7398
- Loading branch information
Showing
6 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import {type QueryParams, type SanityClient} from '@sanity/client' | ||
import {sortedIndex} from 'lodash' | ||
import {of} from 'rxjs' | ||
import {distinctUntilChanged, filter, map, mergeMap, scan, tap} from 'rxjs/operators' | ||
|
||
export type DocumentIdSetObserverState = { | ||
status: 'reconnecting' | 'connected' | ||
documentIds: string[] | ||
} | ||
|
||
interface LiveDocumentIdSetOptions { | ||
insert?: 'sorted' | 'prepend' | 'append' | ||
} | ||
|
||
export function createDocumentIdSetObserver(client: SanityClient) { | ||
return function observe( | ||
queryFilter: string, | ||
params?: QueryParams, | ||
options: LiveDocumentIdSetOptions = {}, | ||
) { | ||
const {insert: insertOption = 'sorted'} = options | ||
|
||
const query = `*[${queryFilter}]._id` | ||
function fetchFilter() { | ||
return client.observable | ||
.fetch(query, params, { | ||
tag: 'preview.observe-document-set.fetch', | ||
}) | ||
.pipe( | ||
tap((result) => { | ||
if (!Array.isArray(result)) { | ||
throw new Error( | ||
`Expected query to return array of documents, but got ${typeof result}`, | ||
) | ||
} | ||
}), | ||
) | ||
} | ||
return client.observable | ||
.listen(query, params, { | ||
visibility: 'transaction', | ||
events: ['welcome', 'mutation', 'reconnect'], | ||
includeResult: false, | ||
includeMutations: false, | ||
tag: 'preview.observe-document-set.listen', | ||
}) | ||
.pipe( | ||
mergeMap((event) => { | ||
return event.type === 'welcome' | ||
? fetchFilter().pipe(map((result) => ({type: 'fetch' as const, result}))) | ||
: of(event) | ||
}), | ||
scan( | ||
( | ||
state: DocumentIdSetObserverState | undefined, | ||
event, | ||
): DocumentIdSetObserverState | undefined => { | ||
if (event.type === 'reconnect') { | ||
return { | ||
documentIds: state?.documentIds || [], | ||
...state, | ||
status: 'reconnecting' as const, | ||
} | ||
} | ||
if (event.type === 'fetch') { | ||
return {...state, status: 'connected' as const, documentIds: event.result} | ||
} | ||
if (event.type === 'mutation') { | ||
if (event.transition === 'update') { | ||
// ignore updates, as we're only interested in documents appearing and disappearing from the set | ||
return state | ||
} | ||
if (event.transition === 'appear') { | ||
return { | ||
status: 'connected', | ||
documentIds: insert(state?.documentIds || [], event.documentId, insertOption), | ||
} | ||
} | ||
if (event.transition === 'disappear') { | ||
return { | ||
status: 'connected', | ||
documentIds: state?.documentIds | ||
? state.documentIds.filter((id) => id !== event.documentId) | ||
: [], | ||
} | ||
} | ||
} | ||
return state | ||
}, | ||
undefined, | ||
), | ||
distinctUntilChanged(), | ||
filter( | ||
(state: DocumentIdSetObserverState | undefined): state is DocumentIdSetObserverState => | ||
state !== undefined, | ||
), | ||
) | ||
} | ||
} | ||
|
||
function insert<T>(array: T[], element: T, strategy: 'sorted' | 'prepend' | 'append') { | ||
let index | ||
if (strategy === 'prepend') { | ||
index = 0 | ||
} else if (strategy === 'append') { | ||
index = array.length | ||
} else { | ||
index = sortedIndex(array, element) | ||
} | ||
|
||
return array.toSpliced(index, 0, element) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {type QueryParams} from '@sanity/client' | ||
import {useMemo} from 'react' | ||
import {useObservable} from 'react-rx' | ||
import {scan} from 'rxjs/operators' | ||
|
||
import {useDocumentPreviewStore} from '../store/_legacy/datastores' | ||
import {type DocumentIdSetObserverState} from './liveDocumentIdSet' | ||
|
||
const INITIAL_STATE = {status: 'loading' as const, documentIds: []} | ||
|
||
export type LiveDocumentSetState = | ||
| {status: 'loading'; documentIds: string[]} | ||
| DocumentIdSetObserverState | ||
|
||
/** | ||
* @internal | ||
* @beta | ||
* Returns document ids that matches the provided GROQ-filter, and loading state | ||
* The document ids are returned in ascending order and will update in real-time | ||
* Whenever a document appears or disappears from the set, a new array with the updated set of IDs will be returned. | ||
* This provides a lightweight way of subscribing to a list of ids for simple cases where you just want the documents ids | ||
* that matches a particular filter. | ||
*/ | ||
export function useLiveDocumentIdSet( | ||
filter: string, | ||
params?: QueryParams, | ||
options: { | ||
// how to insert new document ids. Defaults to `sorted` | ||
insert?: 'sorted' | 'prepend' | 'append' | ||
} = {}, | ||
) { | ||
const documentPreviewStore = useDocumentPreviewStore() | ||
const observable = useMemo( | ||
() => | ||
documentPreviewStore.unstable_observeDocumentIdSet(filter, params, options).pipe( | ||
scan( | ||
(currentState: LiveDocumentSetState, nextState) => ({ | ||
...currentState, | ||
...nextState, | ||
}), | ||
INITIAL_STATE, | ||
), | ||
), | ||
[documentPreviewStore, filter, params, options], | ||
) | ||
return useObservable(observable, INITIAL_STATE) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import {type QueryParams} from '@sanity/client' | ||
import {type SanityDocument} from '@sanity/types' | ||
import {useMemo} from 'react' | ||
import {useObservable} from 'react-rx' | ||
import {map} from 'rxjs/operators' | ||
import {mergeMapArray} from 'rxjs-mergemap-array' | ||
|
||
import {useDocumentPreviewStore} from '../store' | ||
|
||
const INITIAL_VALUE = {loading: true, documents: []} | ||
|
||
/** | ||
* @internal | ||
* @beta | ||
* | ||
* Observes a set of documents matching the filter and returns an array of complete documents | ||
* A new array will be pushed whenever a document in the set changes | ||
* Document ids are returned in ascending order | ||
* Any sorting beyond that must happen client side | ||
*/ | ||
export function useLiveDocumentSet( | ||
groqFilter: string, | ||
params?: QueryParams, | ||
): {loading: boolean; documents: SanityDocument[]} { | ||
const documentPreviewStore = useDocumentPreviewStore() | ||
const observable = useMemo(() => { | ||
return documentPreviewStore.unstable_observeDocumentIdSet(groqFilter, params).pipe( | ||
map((state) => (state.documentIds || []) as string[]), | ||
mergeMapArray((id) => documentPreviewStore.unstable_observeDocument(id)), | ||
map((docs) => ({loading: false, documents: docs as SanityDocument[]})), | ||
) | ||
}, [documentPreviewStore, groqFilter, params]) | ||
return useObservable(observable, INITIAL_VALUE) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.