@sanity/client 8.0.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -0
- package/dist/{browserUpload-CQgx9YYo.js → browserUpload-2tz6Sdqp.js} +4 -3
- package/dist/browserUpload-2tz6Sdqp.js.map +1 -0
- package/dist/{browserUpload-icWlVP15.js → browserUpload-CwpNx7Vl.js} +4 -3
- package/dist/browserUpload-CwpNx7Vl.js.map +1 -0
- package/dist/{config-a8VajuEY.js → config-3wiPP-sZ.js} +2 -2
- package/dist/config-3wiPP-sZ.js.map +1 -0
- package/dist/csm.js +2 -2
- package/dist/csm.js.map +1 -1
- package/dist/index.d.ts +17 -11
- package/dist/index.js +610 -136
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +918 -26
- package/dist/index.node.js +559 -66
- package/dist/index.node.js.map +1 -1
- package/dist/media-library.d.ts +1 -1
- package/dist/{request-CJxcN16k.js → request-BhMuKj0D.js} +10 -9
- package/dist/request-BhMuKj0D.js.map +1 -0
- package/dist/{request-k7VS_NnC.js → request-SnMg7nUX.js} +10 -9
- package/dist/request-SnMg7nUX.js.map +1 -0
- package/dist/{resolveEditInfo-sq7yF78q.js → resolveEditInfo-Cz-smq3a.js} +17 -3
- package/dist/resolveEditInfo-Cz-smq3a.js.map +1 -0
- package/dist/stega.js +1 -1
- package/dist/{stegaEncodeSourceMap-DkoIlutY.js → stegaEncodeSourceMap-DbM2fTN4.js} +8 -2
- package/dist/stegaEncodeSourceMap-DbM2fTN4.js.map +1 -0
- package/dist/{stegaEncodeSourceMap-B2fGArSf.js → stegaEncodeSourceMap-YR3NQ3iz.js} +2 -2
- package/dist/{stegaEncodeSourceMap-B2fGArSf.js.map → stegaEncodeSourceMap-YR3NQ3iz.js.map} +1 -1
- package/dist/{types-CUxZSgB2.d.ts → types-nJhm5Nyq.d.ts} +910 -24
- package/package.json +26 -11
- package/src/SanityClient.ts +39 -20
- package/src/assets/AssetsClient.ts +54 -5
- package/src/collaboration/CollaborationCommentsClient.ts +387 -0
- package/src/collaboration/comments.ts +313 -0
- package/src/collaboration/types.ts +252 -0
- package/src/csm/applySourceDocuments.ts +2 -4
- package/src/csm/draftUtils.ts +23 -4
- package/src/data/dataMethods.ts +9 -20
- package/src/data/eventsource.ts +71 -41
- package/src/data/listen.ts +20 -5
- package/src/data/live.ts +17 -9
- package/src/data/resolveEventSourceFetch.ts +9 -1
- package/src/defineCreateClient.ts +5 -1
- package/src/functions/FunctionsClient.ts +66 -0
- package/src/functions/invoke.ts +176 -0
- package/src/http/browserUpload.ts +1 -0
- package/src/http/errors.ts +2 -1
- package/src/http/request.ts +8 -14
- package/src/mediaLibrary/MediaLibraryVideoClient.ts +1 -1
- package/src/types.ts +420 -4
- package/src/validators.ts +1 -1
- package/src/warnings.ts +7 -1
- package/dist/browserUpload-CQgx9YYo.js.map +0 -1
- package/dist/browserUpload-icWlVP15.js.map +0 -1
- package/dist/config-a8VajuEY.js.map +0 -1
- package/dist/request-CJxcN16k.js.map +0 -1
- package/dist/request-k7VS_NnC.js.map +0 -1
- package/dist/resolveEditInfo-sq7yF78q.js.map +0 -1
- package/dist/stegaEncodeSourceMap-DkoIlutY.js.map +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Any,
|
|
3
|
+
ListenOptions,
|
|
4
|
+
RequestOptions,
|
|
5
|
+
ResumableListenOptions,
|
|
6
|
+
SanityDocument,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/** @internal */
|
|
10
|
+
export const possibleRequestOptions = ['headers', 'signal', 'tag', 'timeout', 'token'] as const
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Request options honored by the collaboration comments methods.
|
|
14
|
+
*
|
|
15
|
+
* @alpha
|
|
16
|
+
*/
|
|
17
|
+
export type CollaborationCommentsRequestOptions = Pick<
|
|
18
|
+
RequestOptions,
|
|
19
|
+
(typeof possibleRequestOptions)[number]
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for collaboration comments write methods.
|
|
24
|
+
*
|
|
25
|
+
* @alpha
|
|
26
|
+
*/
|
|
27
|
+
export type CollaborationCommentsWriteOptions = CollaborationCommentsRequestOptions & {
|
|
28
|
+
/** Transaction ID to associate the write with */
|
|
29
|
+
transactionId?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Listener options for `collaboration.comments.listen`.
|
|
34
|
+
*
|
|
35
|
+
* `includeAllVersions` is left out: comments are stored as `sanity.comment`
|
|
36
|
+
* documents with no drafts or versions, so it would never make a difference.
|
|
37
|
+
*
|
|
38
|
+
* @alpha
|
|
39
|
+
*/
|
|
40
|
+
export type CollaborationCommentsListenOptions =
|
|
41
|
+
| Omit<ListenOptions, 'includeAllVersions'>
|
|
42
|
+
| Omit<ResumableListenOptions, 'includeAllVersions'>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Status of a comment thread. Replies always share the status of their parent comment.
|
|
46
|
+
*
|
|
47
|
+
* @alpha
|
|
48
|
+
*/
|
|
49
|
+
export type CollaborationCommentStatus = 'open' | 'resolved'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emoji short names that can be used as comment reactions.
|
|
53
|
+
*
|
|
54
|
+
* @alpha
|
|
55
|
+
*/
|
|
56
|
+
export type CollaborationCommentReactionShortName =
|
|
57
|
+
| ':-1:'
|
|
58
|
+
| ':+1:'
|
|
59
|
+
| ':eyes:'
|
|
60
|
+
| ':heart:'
|
|
61
|
+
| ':heavy_plus_sign:'
|
|
62
|
+
| ':rocket:'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A single Portable Text block, as used in comment messages and content snapshots.
|
|
66
|
+
*
|
|
67
|
+
* @alpha
|
|
68
|
+
*/
|
|
69
|
+
export interface CollaborationCommentPortableTextBlock {
|
|
70
|
+
_type: string
|
|
71
|
+
children: Array<{_type: string; [key: string]: Any}>
|
|
72
|
+
[key: string]: Any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Comment message, as an array of Portable Text blocks.
|
|
77
|
+
*
|
|
78
|
+
* @alpha
|
|
79
|
+
*/
|
|
80
|
+
export type CollaborationCommentMessage = CollaborationCommentPortableTextBlock[]
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The text an inline comment was anchored to, resolved by the API when the
|
|
84
|
+
* comment was created.
|
|
85
|
+
*
|
|
86
|
+
* Holds one entry per Portable Text block the selection spans, keyed by the
|
|
87
|
+
* block it came from. `text` is the plain text of that block with the selected
|
|
88
|
+
* part wrapped in the marker characters `\uF000` (start) and `\uF001` (end).
|
|
89
|
+
*
|
|
90
|
+
* @alpha
|
|
91
|
+
*/
|
|
92
|
+
export interface CollaborationCommentSelection {
|
|
93
|
+
type: 'text'
|
|
94
|
+
value: {_key: string; text: string}[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A comment document, as stored by the Comments API.
|
|
99
|
+
*
|
|
100
|
+
* @alpha
|
|
101
|
+
*/
|
|
102
|
+
export interface CollaborationCommentDocument extends SanityDocument {
|
|
103
|
+
_type: 'sanity.comment'
|
|
104
|
+
_system?: {
|
|
105
|
+
/** ID of the user that created the comment */
|
|
106
|
+
createdBy?: string
|
|
107
|
+
}
|
|
108
|
+
/** ID shared by a top-level comment and all of its replies */
|
|
109
|
+
threadId?: string
|
|
110
|
+
/** Set on replies, pointing to the comment being replied to */
|
|
111
|
+
parentCommentId?: string
|
|
112
|
+
message: CollaborationCommentMessage
|
|
113
|
+
reactions: {
|
|
114
|
+
_key: string
|
|
115
|
+
shortName: CollaborationCommentReactionShortName
|
|
116
|
+
userId: string
|
|
117
|
+
addedAt: string
|
|
118
|
+
}[]
|
|
119
|
+
/** Arbitrary metadata stored with the comment by the creating application */
|
|
120
|
+
context?: Record<string, unknown>
|
|
121
|
+
target: {
|
|
122
|
+
/** Global document reference (`resourceType:resourceId:documentId`, using the published document ID) */
|
|
123
|
+
document: {
|
|
124
|
+
_ref: `${string}:${string}:${string}`
|
|
125
|
+
_type: 'globalDocumentReference'
|
|
126
|
+
_weak: true
|
|
127
|
+
}
|
|
128
|
+
documentType: string
|
|
129
|
+
/** The exact document ID the comment was created against, e.g. a draft or version ID */
|
|
130
|
+
sourceDocumentId: string
|
|
131
|
+
documentRevisionId?: string
|
|
132
|
+
/**
|
|
133
|
+
* Set for field and inline comments. `field` is the `path` the comment was
|
|
134
|
+
* created with; `selection` is set for inline comments only.
|
|
135
|
+
*/
|
|
136
|
+
path?: {
|
|
137
|
+
field: string
|
|
138
|
+
selection?: CollaborationCommentSelection
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Copy of the commented content, as it looked when the comment was created.
|
|
143
|
+
* Set for inline comments only, and holds just the selected fragment of each
|
|
144
|
+
* Portable Text block the selection spans.
|
|
145
|
+
*/
|
|
146
|
+
contentSnapshot?: CollaborationCommentPortableTextBlock[]
|
|
147
|
+
status: CollaborationCommentStatus
|
|
148
|
+
/** Set when the message has been updated after creation */
|
|
149
|
+
lastEditedAt?: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Inline text selection within a Portable Text field.
|
|
154
|
+
* Each endpoint pairs the `_key` of a Portable Text block with a character
|
|
155
|
+
* offset into that block's plain text.
|
|
156
|
+
*
|
|
157
|
+
* @alpha
|
|
158
|
+
*/
|
|
159
|
+
export interface CollaborationCommentRange {
|
|
160
|
+
start: {_key: string; offset: number}
|
|
161
|
+
end: {_key: string; offset: number}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Target for a top-level comment. Inline selections require both `path` and
|
|
166
|
+
* `range`; field-level comments may set `path` alone.
|
|
167
|
+
*
|
|
168
|
+
* The created comment stores this in a different shape: `path` becomes
|
|
169
|
+
* `target.path.field`, and `range` is resolved against the document into
|
|
170
|
+
* `target.path.selection` and `contentSnapshot` rather than being stored.
|
|
171
|
+
*
|
|
172
|
+
* @alpha
|
|
173
|
+
*/
|
|
174
|
+
export type CollaborationCommentTarget = {
|
|
175
|
+
documentId: string
|
|
176
|
+
documentType: string
|
|
177
|
+
documentRevisionId?: string
|
|
178
|
+
} & (
|
|
179
|
+
| {
|
|
180
|
+
/** Path to the field containing the inline comment selection */
|
|
181
|
+
path: string
|
|
182
|
+
range: CollaborationCommentRange
|
|
183
|
+
}
|
|
184
|
+
| {
|
|
185
|
+
/** Path to the commented field */
|
|
186
|
+
path?: string
|
|
187
|
+
range?: never
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Comment to create with `collaboration.comments.create`.
|
|
193
|
+
*
|
|
194
|
+
* A top-level comment requires `target`; a reply requires `parentCommentId` (never both).
|
|
195
|
+
* Replies inherit `target`, `status`, and `threadId` from the parent comment.
|
|
196
|
+
*
|
|
197
|
+
* ### Examples
|
|
198
|
+
*
|
|
199
|
+
* #### Top-level comment
|
|
200
|
+
* ```ts
|
|
201
|
+
* // `message` is an array of Portable Text blocks
|
|
202
|
+
* await client.collaboration.comments.create({
|
|
203
|
+
* message,
|
|
204
|
+
* target: {documentId: 'doc-1', documentType: 'article'},
|
|
205
|
+
* })
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* #### Reply
|
|
209
|
+
* ```ts
|
|
210
|
+
* await client.collaboration.comments.create({
|
|
211
|
+
* message,
|
|
212
|
+
* parentCommentId: 'comment-1',
|
|
213
|
+
* })
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @alpha
|
|
217
|
+
*/
|
|
218
|
+
export type CollaborationCommentCreate = {
|
|
219
|
+
/** Provide to control the ID of the created comment document */
|
|
220
|
+
_id?: string
|
|
221
|
+
message: CollaborationCommentMessage
|
|
222
|
+
context?: Record<string, unknown>
|
|
223
|
+
} & (
|
|
224
|
+
| {
|
|
225
|
+
target: CollaborationCommentTarget
|
|
226
|
+
threadId?: string
|
|
227
|
+
parentCommentId?: never
|
|
228
|
+
}
|
|
229
|
+
| {
|
|
230
|
+
parentCommentId: string
|
|
231
|
+
target?: never
|
|
232
|
+
threadId?: never
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Fields that can be updated on an existing comment.
|
|
238
|
+
*
|
|
239
|
+
* @alpha
|
|
240
|
+
*/
|
|
241
|
+
export interface CollaborationCommentUpdate {
|
|
242
|
+
/** Replaces the current message */
|
|
243
|
+
message?: CollaborationCommentMessage
|
|
244
|
+
/** Cascades to the comment's replies */
|
|
245
|
+
status?: CollaborationCommentStatus
|
|
246
|
+
/**
|
|
247
|
+
* Re-anchors the comment within the field and source document it already
|
|
248
|
+
* targets. Pass `null` to remove the selection and leave a field-level
|
|
249
|
+
* comment.
|
|
250
|
+
*/
|
|
251
|
+
range?: CollaborationCommentRange | null
|
|
252
|
+
}
|
|
@@ -3,12 +3,10 @@ import {parseJsonPath} from './jsonPath'
|
|
|
3
3
|
import {resolveMapping} from './resolveMapping'
|
|
4
4
|
import * as paths from './studioPath'
|
|
5
5
|
import type {
|
|
6
|
-
Any,
|
|
7
6
|
ApplySourceDocumentsUpdateFunction,
|
|
8
7
|
ClientPerspective,
|
|
9
8
|
ContentSourceMap,
|
|
10
9
|
ContentSourceMapDocuments,
|
|
11
|
-
Path,
|
|
12
10
|
SanityDocument,
|
|
13
11
|
} from './types'
|
|
14
12
|
import {walkMap} from './walkMap'
|
|
@@ -55,7 +53,7 @@ export function applySourceDocuments<Result = unknown>(
|
|
|
55
53
|
|
|
56
54
|
if (sourceDocument) {
|
|
57
55
|
const parsedPath = parseJsonPath(sourcePath + pathSuffix)
|
|
58
|
-
const stringifiedPath = paths.toString(parsedPath
|
|
56
|
+
const stringifiedPath = paths.toString(parsedPath)
|
|
59
57
|
const cachedDocument = cachedDocuments[mapping.source.document]
|
|
60
58
|
|
|
61
59
|
if (!cachedDocument) {
|
|
@@ -67,7 +65,7 @@ export function applySourceDocuments<Result = unknown>(
|
|
|
67
65
|
: value
|
|
68
66
|
return value === changedValue
|
|
69
67
|
? value
|
|
70
|
-
: updateFn<Result[keyof Result]>(changedValue as
|
|
68
|
+
: updateFn<Result[keyof Result]>(changedValue as Result[keyof Result], {
|
|
71
69
|
cachedDocument,
|
|
72
70
|
previousValue: value as Result[keyof Result],
|
|
73
71
|
sourceDocument,
|
package/src/csm/draftUtils.ts
CHANGED
|
@@ -32,11 +32,24 @@ export function isPublishedId(id: string): id is PublishedId {
|
|
|
32
32
|
return !isDraftId(id) && !isVersionId(id)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A phantom brand like `DraftId` has no runtime representation, so it can never be produced
|
|
37
|
+
* by narrowing a string - there's nothing to check. These two functions are the only places
|
|
38
|
+
* allowed to assert a plain string into a branded id.
|
|
39
|
+
*/
|
|
40
|
+
function asDraftId(value: string): DraftId {
|
|
41
|
+
return value as DraftId
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function asPublishedId(value: string): PublishedId {
|
|
45
|
+
return value as PublishedId
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/** @internal */
|
|
36
49
|
export function getDraftId(id: string): DraftId {
|
|
37
50
|
if (isVersionId(id)) {
|
|
38
51
|
const publishedId = getPublishedId(id)
|
|
39
|
-
return (DRAFTS_PREFIX + publishedId)
|
|
52
|
+
return asDraftId(DRAFTS_PREFIX + publishedId)
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
return isDraftId(id) ? id : ((DRAFTS_PREFIX + id) as DraftId)
|
|
@@ -69,12 +82,18 @@ export function getVersionFromId(id: string): string | undefined {
|
|
|
69
82
|
export function getPublishedId(id: string): PublishedId {
|
|
70
83
|
if (isVersionId(id)) {
|
|
71
84
|
// make sure to only remove the versions prefix and the bundle name
|
|
72
|
-
return id.split(PATH_SEPARATOR).slice(2).join(PATH_SEPARATOR)
|
|
85
|
+
return asPublishedId(id.split(PATH_SEPARATOR).slice(2).join(PATH_SEPARATOR))
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
if (isDraftId(id)) {
|
|
76
|
-
return id.slice(DRAFTS_PREFIX.length)
|
|
89
|
+
return asPublishedId(id.slice(DRAFTS_PREFIX.length))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isPublishedId(id)) {
|
|
93
|
+
return id
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
|
|
96
|
+
// Unreachable: `isPublishedId` is defined as `!isDraftId(id) && !isVersionId(id)`, both of
|
|
97
|
+
// which were already checked (and found false) above, so this can never execute.
|
|
98
|
+
throw new Error(`Unable to resolve a published id from "${id}"`)
|
|
80
99
|
}
|
package/src/data/dataMethods.ts
CHANGED
|
@@ -73,7 +73,13 @@ const indexBy = (docs: Any[], attr: Any) =>
|
|
|
73
73
|
return indexed
|
|
74
74
|
}, Object.create(null))
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Encoded query strings longer than this are sent as a `POST` body rather than
|
|
78
|
+
* in the request URL.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export const getQuerySizeLimit = 11264
|
|
77
83
|
|
|
78
84
|
/**
|
|
79
85
|
* Resolve the effective stega config, cleaned params, response mapper and
|
|
@@ -667,23 +673,6 @@ function _mapDataResponse(
|
|
|
667
673
|
/**
|
|
668
674
|
* @internal
|
|
669
675
|
*/
|
|
670
|
-
export function _dataRequestObservable(
|
|
671
|
-
client: Client,
|
|
672
|
-
httpRequest: HttpRequest,
|
|
673
|
-
endpoint: string,
|
|
674
|
-
body: Any,
|
|
675
|
-
options: Any = {},
|
|
676
|
-
): Observable<Any> {
|
|
677
|
-
return _observe(options.signal, (signal) =>
|
|
678
|
-
_dataRequest(client, httpRequest, endpoint, body, {...options, signal}),
|
|
679
|
-
)
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Promise-based sibling of {@link _dataRequestObservable}.
|
|
684
|
-
*
|
|
685
|
-
* @internal
|
|
686
|
-
*/
|
|
687
676
|
export function _dataRequest(
|
|
688
677
|
client: Client,
|
|
689
678
|
httpRequest: HttpRequest,
|
|
@@ -1139,7 +1128,7 @@ export function _prepareRequest(client: Client, options: RequestObservableOption
|
|
|
1139
1128
|
*
|
|
1140
1129
|
* @internal
|
|
1141
1130
|
*/
|
|
1142
|
-
|
|
1131
|
+
function _observe<R>(
|
|
1143
1132
|
userSignal: AbortSignal | undefined,
|
|
1144
1133
|
run: (signal: AbortSignal) => Promise<R>,
|
|
1145
1134
|
): Observable<R> {
|
|
@@ -1171,7 +1160,7 @@ export function _observe<R>(
|
|
|
1171
1160
|
*/
|
|
1172
1161
|
export function _request<R>(client: Client, httpRequest: HttpRequest, options: Any): Promise<R> {
|
|
1173
1162
|
const reqOptions = _prepareRequest(client, options)
|
|
1174
|
-
return httpRequest(reqOptions).then((body) => body as R)
|
|
1163
|
+
return httpRequest(reqOptions, client.config().requestHandler).then((body) => body as R)
|
|
1175
1164
|
}
|
|
1176
1165
|
|
|
1177
1166
|
/**
|
package/src/data/eventsource.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import type {ErrorEvent, EventSourceConstructor} from 'eventsource'
|
|
1
2
|
import {defer, isObservable, mergeMap, Observable, of} from 'rxjs'
|
|
2
3
|
|
|
3
4
|
import {formatQueryParseError, isQueryParseError} from '../http/errors'
|
|
4
|
-
import {
|
|
5
|
+
import {isRecord} from '../util/isRecord'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* @public
|
|
8
8
|
* Thrown when the EventSource connection could not be established, or was rejected by the server.
|
|
9
9
|
* Transient failures (network drops, 5xx, 408, 429) are reconnected internally and emitted as
|
|
10
10
|
* `reconnect` events; a permanent rejection (any other 4xx, eg an expired token) errors the
|
|
11
11
|
* stream with this class so consumers can react — check `status` for the rejection code.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
12
14
|
*/
|
|
13
15
|
export class ConnectionFailedError extends Error {
|
|
14
16
|
readonly name = 'ConnectionFailedError'
|
|
@@ -28,8 +30,9 @@ export class ConnectionFailedError extends Error {
|
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* The listener has been told to explicitly disconnect.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* This is a rare situation, but may occur if the API knows reconnect attempts will fail,
|
|
34
|
+
* eg in the case of a deleted dataset, a blocked project or similar events.
|
|
35
|
+
*
|
|
33
36
|
* @public
|
|
34
37
|
*/
|
|
35
38
|
export class DisconnectError extends Error {
|
|
@@ -42,8 +45,9 @@ export class DisconnectError extends Error {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* @public
|
|
46
48
|
* The server sent a `channelError` message. Usually indicative of a bad or malformed request
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
47
51
|
*/
|
|
48
52
|
export class ChannelError extends Error {
|
|
49
53
|
readonly name = 'ChannelError'
|
|
@@ -55,8 +59,9 @@ export class ChannelError extends Error {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
* @public
|
|
59
62
|
* The server sent an `error`-event to tell the client that an unexpected error has happened.
|
|
63
|
+
*
|
|
64
|
+
* @public
|
|
60
65
|
*/
|
|
61
66
|
export class MessageError extends Error {
|
|
62
67
|
readonly name = 'MessageError'
|
|
@@ -68,8 +73,9 @@ export class MessageError extends Error {
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
/**
|
|
71
|
-
* @public
|
|
72
76
|
* An error occurred while parsing the message sent by the server as JSON. Should normally not happen.
|
|
77
|
+
*
|
|
78
|
+
* @public
|
|
73
79
|
*/
|
|
74
80
|
export class MessageParseError extends Error {
|
|
75
81
|
readonly name = 'MessageParseError'
|
|
@@ -95,7 +101,7 @@ export type EventSourceEvent<Name extends string> = ServerSentEvent<Name>
|
|
|
95
101
|
/**
|
|
96
102
|
* @internal
|
|
97
103
|
*/
|
|
98
|
-
export type EventSourceInstance = InstanceType<
|
|
104
|
+
export type EventSourceInstance = InstanceType<EventSourceConstructor>
|
|
99
105
|
|
|
100
106
|
/**
|
|
101
107
|
* Sanity API specific EventSource handler shared between the listen and live APIs
|
|
@@ -123,9 +129,7 @@ export function connectEventSource<EventName extends string>(
|
|
|
123
129
|
return defer(() => {
|
|
124
130
|
const es = initEventSource()
|
|
125
131
|
return isObservable(es) ? es : of(es)
|
|
126
|
-
}).pipe(mergeMap((es) => connectWithESInstance(es, events)))
|
|
127
|
-
ServerSentEvent<EventName>
|
|
128
|
-
>
|
|
132
|
+
}).pipe(mergeMap((es) => connectWithESInstance(es, events)))
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
/**
|
|
@@ -140,21 +144,30 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
140
144
|
events: EventTypeName[],
|
|
141
145
|
) {
|
|
142
146
|
return new Observable<EventSourceEvent<EventTypeName>>((observer) => {
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
// Events actually requested by the caller. Backs `isRequestedEvent`, the type
|
|
148
|
+
// guard used below to narrow plain strings (eg `message.type`) to `EventTypeName`
|
|
149
|
+
// without a cast.
|
|
150
|
+
const requestedEvents = new Set<string>(events)
|
|
151
|
+
const isRequestedEvent = (type: string): type is EventTypeName => requestedEvents.has(type)
|
|
152
|
+
const emitOpen = isRequestedEvent('open')
|
|
145
153
|
|
|
146
154
|
// EventSource will emit a regular Event if it fails to connect, however the API may also emit an `error` MessageEvent
|
|
147
155
|
// So we need to handle both cases
|
|
148
|
-
function onError(evt:
|
|
156
|
+
function onError(evt: ErrorEvent | MessageEvent) {
|
|
149
157
|
// If the event has a `data` property, then it`s a MessageEvent emitted by the API and we should forward the error
|
|
150
158
|
if ('data' in evt) {
|
|
151
|
-
const [parseError, event] = parseEvent(evt
|
|
159
|
+
const [parseError, event] = parseEvent(evt)
|
|
152
160
|
observer.error(
|
|
153
|
-
parseError
|
|
161
|
+
parseError || !event
|
|
154
162
|
? new MessageParseError('Unable to parse EventSource error message', {
|
|
155
163
|
cause: parseError,
|
|
156
164
|
})
|
|
157
|
-
: new MessageError(
|
|
165
|
+
: new MessageError(
|
|
166
|
+
isRecord(event.data) && typeof event.data.message === 'string'
|
|
167
|
+
? event.data.message
|
|
168
|
+
: '',
|
|
169
|
+
event,
|
|
170
|
+
),
|
|
158
171
|
)
|
|
159
172
|
return
|
|
160
173
|
}
|
|
@@ -169,29 +182,35 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
169
182
|
// regardless of readyState — implementations disagree on whether the connection
|
|
170
183
|
// closes before or after the error event is dispatched — and let
|
|
171
184
|
// `reconnectOnConnectionFailure` classify it (4xx fatal, otherwise retried).
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
if (evt.code !== undefined) {
|
|
186
|
+
observer.error(
|
|
187
|
+
new ConnectionFailedError('EventSource connection failed', {status: evt.code}),
|
|
188
|
+
)
|
|
176
189
|
return
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
if (es.readyState === es.CLOSED) {
|
|
180
193
|
// In these cases we'll signal to consumers (via the error path) that a retry/reconnect is needed.
|
|
181
194
|
observer.error(new ConnectionFailedError('EventSource connection failed'))
|
|
182
|
-
} else
|
|
183
|
-
|
|
195
|
+
} else {
|
|
196
|
+
const type = 'reconnect'
|
|
197
|
+
if (isRequestedEvent(type)) {
|
|
198
|
+
observer.next({type})
|
|
199
|
+
}
|
|
184
200
|
}
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
function onOpen() {
|
|
188
204
|
// The open event of the EventSource API is fired when a connection with an event source is opened.
|
|
189
|
-
|
|
205
|
+
const type = 'open'
|
|
206
|
+
if (isRequestedEvent(type)) {
|
|
207
|
+
observer.next({type})
|
|
208
|
+
}
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
function onMessage(message: MessageEvent) {
|
|
193
212
|
const [parseError, event] = parseEvent(message)
|
|
194
|
-
if (parseError) {
|
|
213
|
+
if (parseError || !event) {
|
|
195
214
|
observer.error(
|
|
196
215
|
new MessageParseError('Unable to parse EventSource message', {cause: parseError}),
|
|
197
216
|
)
|
|
@@ -212,17 +231,25 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
212
231
|
observer.error(
|
|
213
232
|
new DisconnectError(
|
|
214
233
|
`Server disconnected client: ${
|
|
215
|
-
(event.data
|
|
234
|
+
(isRecord(event.data) &&
|
|
235
|
+
typeof event.data.reason === 'string' &&
|
|
236
|
+
event.data.reason) ||
|
|
237
|
+
'unknown error'
|
|
216
238
|
}`,
|
|
217
239
|
),
|
|
218
240
|
)
|
|
219
241
|
return
|
|
220
242
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
243
|
+
// `onMessage` is only ever registered for `REQUIRED_EVENTS` (handled above, and always
|
|
244
|
+
// returned from before reaching here) and the caller-requested `events` (see
|
|
245
|
+
// `cleanedEvents` below), so `message.type` is guaranteed to be a requested event here.
|
|
246
|
+
if (isRequestedEvent(message.type)) {
|
|
247
|
+
observer.next({
|
|
248
|
+
type: message.type,
|
|
249
|
+
id: message.lastEventId,
|
|
250
|
+
...(event.data ? {data: event.data} : {}),
|
|
251
|
+
})
|
|
252
|
+
}
|
|
226
253
|
}
|
|
227
254
|
|
|
228
255
|
es.addEventListener('error', onError)
|
|
@@ -251,7 +278,7 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
251
278
|
|
|
252
279
|
function parseEvent(
|
|
253
280
|
message: MessageEvent,
|
|
254
|
-
): [null, {type: string; id: string; data?: unknown}] | [
|
|
281
|
+
): [null, {type: string; id: string; data?: unknown}] | [unknown, null] {
|
|
255
282
|
try {
|
|
256
283
|
const data = typeof message.data === 'string' && JSON.parse(message.data)
|
|
257
284
|
return [
|
|
@@ -263,23 +290,26 @@ function parseEvent(
|
|
|
263
290
|
},
|
|
264
291
|
]
|
|
265
292
|
} catch (err) {
|
|
266
|
-
return [err
|
|
293
|
+
return [err, null]
|
|
267
294
|
}
|
|
268
295
|
}
|
|
269
296
|
|
|
270
|
-
function extractErrorMessage(err:
|
|
271
|
-
const error = err.error
|
|
297
|
+
function extractErrorMessage(err: unknown, tag?: string | null): string {
|
|
298
|
+
const error = isRecord(err) ? err.error : undefined
|
|
272
299
|
|
|
273
300
|
if (!error) {
|
|
274
|
-
|
|
301
|
+
const message = isRecord(err) ? err.message : undefined
|
|
302
|
+
return (typeof message === 'string' && message) || 'Unknown listener error'
|
|
275
303
|
}
|
|
276
304
|
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
305
|
+
if (isRecord(error)) {
|
|
306
|
+
if (isQueryParseError(error)) {
|
|
307
|
+
return formatQueryParseError(error, tag)
|
|
308
|
+
}
|
|
280
309
|
|
|
281
|
-
|
|
282
|
-
|
|
310
|
+
if (typeof error.description === 'string') {
|
|
311
|
+
return error.description
|
|
312
|
+
}
|
|
283
313
|
}
|
|
284
314
|
|
|
285
315
|
return typeof error === 'string' ? error : JSON.stringify(error, null, 2)
|
package/src/data/listen.ts
CHANGED
|
@@ -29,9 +29,11 @@ import {resolveEventSourceFetch} from './resolveEventSourceFetch'
|
|
|
29
29
|
// Limit is 16K for a _request_, eg including headers. Have to account for an
|
|
30
30
|
// unknown range of headers, but an average EventSource request from Chrome seems
|
|
31
31
|
// to have around 700 bytes of cruft, so let us account for 1.2K to be "safe"
|
|
32
|
-
|
|
32
|
+
/** @internal */
|
|
33
|
+
export const MAX_URL_LENGTH = 16000 - 1200
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/** @internal */
|
|
36
|
+
export const possibleOptions = [
|
|
35
37
|
'includePreviousRevision',
|
|
36
38
|
'includeResult',
|
|
37
39
|
'includeMutations',
|
|
@@ -42,7 +44,8 @@ const possibleOptions = [
|
|
|
42
44
|
'tag',
|
|
43
45
|
]
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
/** @internal */
|
|
48
|
+
export const defaultOptions = {
|
|
46
49
|
includeResult: true,
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -137,7 +140,7 @@ export function _listen<
|
|
|
137
140
|
opts: Opts = {} as Opts,
|
|
138
141
|
): Observable<ListenEventFromOptions<R, Opts>> {
|
|
139
142
|
const config = this.config()
|
|
140
|
-
const {url,
|
|
143
|
+
const {url, requestTagPrefix} = config
|
|
141
144
|
const tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join('.') : opts.tag
|
|
142
145
|
const options = {...defaults(opts, defaultOptions), tag}
|
|
143
146
|
const listenOpts = pick(options, possibleOptions)
|
|
@@ -150,6 +153,18 @@ export function _listen<
|
|
|
150
153
|
|
|
151
154
|
const listenFor = (options.events ? options.events : ['mutation']) satisfies Opts['events']
|
|
152
155
|
|
|
156
|
+
return _connectListenEventSource<ListenEventFromOptions<R, Opts>>(this, uri, listenFor)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @internal */
|
|
160
|
+
export function _connectListenEventSource<TEvent extends {type: string}>(
|
|
161
|
+
client: SanityClient | ObservableSanityClient,
|
|
162
|
+
uri: string,
|
|
163
|
+
listenFor: string[],
|
|
164
|
+
): Observable<TEvent> {
|
|
165
|
+
const config = client.config()
|
|
166
|
+
const {token, withCredentials, headers: configHeaders} = config
|
|
167
|
+
|
|
153
168
|
const headers: Record<string, string> = {}
|
|
154
169
|
if (token) {
|
|
155
170
|
headers.Authorization = `Bearer ${token}`
|
|
@@ -173,5 +188,5 @@ export function _listen<
|
|
|
173
188
|
type: event.type,
|
|
174
189
|
...('data' in event ? (event.data as object) : {}),
|
|
175
190
|
})),
|
|
176
|
-
) as Observable<
|
|
191
|
+
) as Observable<TEvent>
|
|
177
192
|
}
|