-
Notifications
You must be signed in to change notification settings - Fork 426
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 observing full documents
- Loading branch information
Showing
4 changed files
with
153 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import {type MutationEvent, type SanityClient, type WelcomeEvent} from '@sanity/client' | ||
import {type SanityDocument} from '@sanity/types' | ||
import {memoize, uniq} from 'lodash' | ||
import {type RawPatch} from 'mendoza' | ||
import {EMPTY, finalize, type Observable, of} from 'rxjs' | ||
import {concatMap, map, scan, shareReplay} from 'rxjs/operators' | ||
|
||
import {type ApiConfig} from './types' | ||
import {applyMutationEventEffects} from './utils/applyMendozaPatch' | ||
import {debounceCollect} from './utils/debounceCollect' | ||
|
||
export function createObserveDocument({ | ||
mutationChannel, | ||
client, | ||
}: { | ||
client: SanityClient | ||
mutationChannel: Observable<WelcomeEvent | MutationEvent> | ||
}) { | ||
const getBatchFetcher = memoize( | ||
function getBatchFetcher(apiConfig: {dataset: string; projectId: string}) { | ||
const _client = client.withConfig(apiConfig) | ||
|
||
function batchFetchDocuments(ids: [string][]) { | ||
return _client.observable | ||
.fetch(`*[_id in $ids]`, {ids: uniq(ids.flat())}, {tag: 'preview.observe-document'}) | ||
.pipe( | ||
// eslint-disable-next-line max-nested-callbacks | ||
map((result) => ids.map(([id]) => result.find((r: {_id: string}) => r._id === id))), | ||
) | ||
} | ||
return debounceCollect(batchFetchDocuments, 100) | ||
}, | ||
(apiConfig) => apiConfig.dataset + apiConfig.projectId, | ||
) | ||
|
||
const MEMO: Record<string, Observable<SanityDocument | undefined>> = {} | ||
|
||
function observeDocument(id: string, apiConfig?: ApiConfig) { | ||
const _apiConfig = apiConfig || { | ||
dataset: client.config().dataset!, | ||
projectId: client.config().projectId!, | ||
} | ||
const fetchDocument = getBatchFetcher(_apiConfig) | ||
return mutationChannel.pipe( | ||
concatMap((event) => { | ||
if (event.type === 'welcome') { | ||
return fetchDocument(id).pipe(map((document) => ({type: 'sync' as const, document}))) | ||
} | ||
return event.documentId === id ? of(event) : EMPTY | ||
}), | ||
scan((current: SanityDocument | undefined, event) => { | ||
if (event.type === 'sync') { | ||
return event.document | ||
} | ||
if (event.type === 'mutation') { | ||
return applyMutationEvent(current, event) | ||
} | ||
//@ts-expect-error - this should never happen | ||
throw new Error(`Unexpected event type: "${event.type}"`) | ||
}, undefined), | ||
) | ||
} | ||
return function memoizedObserveDocument(id: string, apiConfig?: ApiConfig) { | ||
const key = apiConfig ? `${id}-${JSON.stringify(apiConfig)}` : id | ||
if (!(key in MEMO)) { | ||
MEMO[key] = observeDocument(id, apiConfig).pipe( | ||
finalize(() => delete MEMO[key]), | ||
shareReplay({bufferSize: 1, refCount: true}), | ||
) | ||
} | ||
return MEMO[key] | ||
} | ||
} | ||
|
||
function applyMutationEvent(current: SanityDocument | undefined, event: MutationEvent) { | ||
if (event.previousRev !== current?._rev) { | ||
console.warn('Document out of sync, skipping mutation') | ||
return current | ||
} | ||
if (!event.effects) { | ||
throw new Error( | ||
'Mutation event is missing effects. Is the listener set up with effectFormat=mendoza?', | ||
) | ||
} | ||
return applyMutationEventEffects( | ||
current, | ||
event as {effects: {apply: RawPatch}; previousRev: string; resultRev: string}, | ||
) | ||
} |
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
44 changes: 44 additions & 0 deletions
44
packages/sanity/src/core/preview/utils/applyMendozaPatch.ts
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,44 @@ | ||
import {type SanityDocument} from '@sanity/types' | ||
import {applyPatch, type RawPatch} from 'mendoza' | ||
|
||
function omitRev(document: SanityDocument | undefined) { | ||
if (document === undefined) { | ||
return undefined | ||
} | ||
const {_rev, ...doc} = document | ||
return doc | ||
} | ||
|
||
/** | ||
* | ||
* @param document - The document to apply the patch to | ||
* @param patch - The mendoza patch to apply | ||
* @param baseRev - The revision of the document that the patch is calculated from. This is used to ensure that the patch is applied to the correct revision of the document | ||
*/ | ||
export function applyMendozaPatch( | ||
document: SanityDocument | undefined, | ||
patch: RawPatch, | ||
baseRev: string, | ||
): SanityDocument | undefined { | ||
if (baseRev !== document?._rev) { | ||
throw new Error( | ||
'Invalid document revision. The provided patch is calculated from a different revision than the current document', | ||
) | ||
} | ||
const next = applyPatch(omitRev(document), patch) | ||
return next === null ? undefined : next | ||
} | ||
|
||
export function applyMutationEventEffects( | ||
document: SanityDocument | undefined, | ||
event: {effects: {apply: RawPatch}; previousRev: string; resultRev: string}, | ||
) { | ||
if (!event.effects) { | ||
throw new Error( | ||
'Mutation event is missing effects. Is the listener set up with effectFormat=mendoza?', | ||
) | ||
} | ||
const next = applyMendozaPatch(document, event.effects.apply, event.previousRev) | ||
// next will be undefined in case of deletion | ||
return next ? {...next, _rev: event.resultRev} : undefined | ||
} |