-
Notifications
You must be signed in to change notification settings - Fork 391
/
availability.ts
153 lines (141 loc) · 4.78 KB
/
availability.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* eslint-disable max-nested-callbacks */
import {combineLatest, defer, from, Observable, of} from 'rxjs'
import {distinctUntilChanged, map, mergeMap, reduce, switchMap} from 'rxjs/operators'
import shallowEquals from 'shallow-equals'
import {flatten, keyBy} from 'lodash'
import {getDraftId, getPublishedId} from '../util/draftUtils'
import {versionedClient} from '../client/versionedClient'
import type {
AvailabilityResponse,
DocumentAvailability,
DraftsModelDocumentAvailability,
} from './types'
import {debounceCollect} from './utils/debounceCollect'
import {
AVAILABILITY_NOT_FOUND,
AVAILABILITY_PERMISSION_DENIED,
AVAILABILITY_READABLE,
} from './constants'
import {observePaths} from './'
const MAX_DOCUMENT_ID_CHUNK_SIZE = 11164
/**
* Returns an observable of metadata for a given drafts model document
* @param id document id
*/
export 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,
}
})
)
}
/**
* 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
* @param id
* @internal
*/
function observeDocumentAvailability(id: string): Observable<DocumentAvailability> {
// check for existence
return observePaths(id, [['_rev']]).pipe(
map((res) => Boolean(res?._rev)),
distinctUntilChanged(),
switchMap((hasRev) => {
return hasRev
? // short circuit: if we can read the _rev field we know it both exists and is readable
of(AVAILABILITY_READABLE)
: // we can't read the _rev field for two possible reasons: 1) the document isn't readable or 2) the document doesn't exist
fetchDocumentReadability(id)
})
)
}
/**
* Takes an array of document IDs and puts them into individual chunks.
* Because document IDs can vary greatly in size, we want to chunk by the length of the
* combined comma-separated ID set. We try to stay within 11164 bytes - this is about the
* same length the Sanity client uses for max query size, and accounts for rather large
* headers to be present - so this _should_ be safe.
*
* @param documentIds Unique document IDs to chunk
* @returns Array of document ID chunks
*/
function chunkDocumentIds(documentIds: string[]): string[][] {
let chunk: string[] = []
let chunkSize = 0
const chunks: string[][] = []
for (const documentId of documentIds) {
// Reached the max length? start a new chunk
if (chunkSize + documentId.length + 1 >= MAX_DOCUMENT_ID_CHUNK_SIZE) {
chunks.push(chunk)
chunk = []
chunkSize = 0
}
chunkSize += documentId.length + 1 // +1 is to account for a comma between IDs
chunk.push(documentId)
}
if (!chunks.includes(chunk)) {
chunks.push(chunk)
}
return chunks
}
/**
* Mutative concat
* @param array
* @param chunks
*/
function mutConcat<T>(array: T[], chunks: T[]) {
array.push(...chunks)
return array
}
const fetchDocumentReadability = debounceCollect(function fetchDocumentReadability(
args: string[][]
): Observable<DocumentAvailability[]> {
const uniqueIds = [...new Set(flatten(args))]
return from(chunkDocumentIds(uniqueIds)).pipe(
mergeMap(fetchDocumentReadabilityChunked, 10),
reduce(mutConcat, []),
map((res) => args.map(([id]) => res[uniqueIds.indexOf(id)]))
)
},
1)
function fetchDocumentReadabilityChunked(ids: string[]): Observable<DocumentAvailability[]> {
return defer(() => {
const requestOptions = {
uri: versionedClient.getDataUrl('doc', ids.join(',')),
json: true,
query: {excludeContent: 'true'},
tag: 'preview.documents-availability',
}
return versionedClient.observable.request<AvailabilityResponse>(requestOptions).pipe(
map((response) => {
const omitted = keyBy(response.omitted || [], (entry) => entry.id)
return ids.map((id) => {
const omittedEntry = omitted[id]
if (!omittedEntry) {
// it's not omitted, so it exists and is readable
return AVAILABILITY_READABLE
}
if (omittedEntry.reason === 'existence') {
return AVAILABILITY_NOT_FOUND
}
if (omittedEntry.reason === 'permission') {
// it's not omitted, so it exists and is readable
return AVAILABILITY_PERMISSION_DENIED
}
throw new Error(`Unexpected reason for omission: "${omittedEntry.reason}"`)
})
})
)
})
}