@sanity/client 7.26.2 → 8.1.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 +63 -341
- package/dist/browserUpload-2tz6Sdqp.js +87 -0
- package/dist/browserUpload-2tz6Sdqp.js.map +1 -0
- package/dist/browserUpload-CwpNx7Vl.js +87 -0
- package/dist/browserUpload-CwpNx7Vl.js.map +1 -0
- package/dist/config-3wiPP-sZ.js +126 -0
- package/dist/config-3wiPP-sZ.js.map +1 -0
- package/dist/csm.d.ts +122 -335
- package/dist/csm.js +79 -91
- package/dist/csm.js.map +1 -1
- package/dist/dist-C9ExSk2R.js +73 -0
- package/dist/dist-C9ExSk2R.js.map +1 -0
- package/dist/dist-Z8cIRxoB.js +69 -0
- package/dist/dist-Z8cIRxoB.js.map +1 -0
- package/dist/index.d.ts +112 -6722
- package/dist/index.js +2950 -2848
- package/dist/index.js.map +1 -1
- package/dist/{index.d.cts → index.node.d.ts} +4529 -5322
- package/dist/index.node.js +3264 -0
- package/dist/index.node.js.map +1 -0
- package/dist/isRecord-Kfmt-nk-.js +7 -0
- package/dist/isRecord-Kfmt-nk-.js.map +1 -0
- package/dist/media-library.d.ts +9 -100
- package/dist/media-library.js +36 -14
- package/dist/media-library.js.map +1 -1
- package/dist/request-BhMuKj0D.js +373 -0
- package/dist/request-BhMuKj0D.js.map +1 -0
- package/dist/request-SnMg7nUX.js +355 -0
- package/dist/request-SnMg7nUX.js.map +1 -0
- package/dist/resolveEditInfo-Cz-smq3a.js +339 -0
- package/dist/resolveEditInfo-Cz-smq3a.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/stega.d.ts +78 -7137
- package/dist/stega.js +27 -20
- package/dist/stega.js.map +1 -1
- package/dist/stegaClean-C18wLWau.js +21 -0
- package/dist/stegaClean-C18wLWau.js.map +1 -0
- package/dist/stegaEncodeSourceMap-DbM2fTN4.js +373 -0
- package/dist/stegaEncodeSourceMap-DbM2fTN4.js.map +1 -0
- package/dist/stegaEncodeSourceMap-YR3NQ3iz.js +163 -0
- package/dist/stegaEncodeSourceMap-YR3NQ3iz.js.map +1 -0
- package/dist/{index.browser.d.cts → types-BODIEY7F.d.ts} +4196 -5258
- package/dist/types-CfGzbXrl.d.ts +108 -0
- package/package.json +80 -142
- package/src/SanityClient.ts +101 -137
- package/src/agent/actions/AgentActionsClient.ts +12 -12
- package/src/agent/actions/commonTypes.ts +2 -1
- package/src/agent/actions/generate.ts +26 -6
- package/src/agent/actions/patch.ts +27 -7
- package/src/agent/actions/prompt.ts +19 -5
- package/src/agent/actions/transform.ts +23 -5
- package/src/agent/actions/translate.ts +23 -5
- package/src/assets/AssetsClient.ts +121 -24
- package/src/csm/applySourceDocuments.ts +5 -5
- package/src/csm/createEditUrl.ts +1 -1
- package/src/csm/draftUtils.ts +23 -5
- package/src/csm/index.ts +2 -2
- package/src/csm/studioPath.ts +2 -1
- package/src/csm/types.ts +8 -2
- package/src/data/dataMethods.ts +784 -349
- package/src/data/eventsource.ts +82 -49
- package/src/data/listen.ts +18 -23
- package/src/data/live.ts +60 -35
- package/src/data/resolveEventSourceFetch.ts +104 -0
- package/src/datasets/DatasetsClient.ts +58 -41
- package/src/defineCreateClient.ts +38 -22
- package/src/functions/FunctionsClient.ts +66 -0
- package/src/functions/invoke.ts +176 -0
- package/src/http/browserMiddleware.ts +7 -1
- package/src/http/browserUpload.ts +163 -0
- package/src/http/errors.ts +61 -6
- package/src/http/nodeMiddleware.ts +94 -30
- package/src/http/request.ts +304 -87
- package/src/http/requestOptions.ts +131 -15
- package/src/{index.browser.ts → index.node.ts} +9 -4
- package/src/index.ts +4 -3
- package/src/mediaLibrary/MediaLibraryVideoClient.ts +27 -12
- package/src/projects/ProjectsClient.ts +18 -16
- package/src/releases/ReleasesClient.ts +38 -33
- package/src/stega/index.ts +1 -2
- package/src/stega/stegaEncodeSourceMap.ts +5 -0
- package/src/stega/types.ts +2 -2
- package/src/types.ts +328 -103
- package/src/users/UsersClient.ts +10 -14
- package/src/util/shareReplayLatest.ts +1 -1
- package/src/validators.ts +1 -1
- package/src/warnings.ts +13 -2
- package/dist/_chunks-cjs/config.cjs +0 -206
- package/dist/_chunks-cjs/config.cjs.map +0 -1
- package/dist/_chunks-cjs/isRecord.cjs +0 -6
- package/dist/_chunks-cjs/isRecord.cjs.map +0 -1
- package/dist/_chunks-cjs/resolveEditInfo.cjs +0 -334
- package/dist/_chunks-cjs/resolveEditInfo.cjs.map +0 -1
- package/dist/_chunks-cjs/stegaClean.cjs +0 -46
- package/dist/_chunks-cjs/stegaClean.cjs.map +0 -1
- package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs +0 -388
- package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs.map +0 -1
- package/dist/_chunks-es/config.js +0 -207
- package/dist/_chunks-es/config.js.map +0 -1
- package/dist/_chunks-es/isRecord.js +0 -7
- package/dist/_chunks-es/isRecord.js.map +0 -1
- package/dist/_chunks-es/resolveEditInfo.js +0 -335
- package/dist/_chunks-es/resolveEditInfo.js.map +0 -1
- package/dist/_chunks-es/stegaClean.js +0 -47
- package/dist/_chunks-es/stegaClean.js.map +0 -1
- package/dist/_chunks-es/stegaEncodeSourceMap.js +0 -389
- package/dist/_chunks-es/stegaEncodeSourceMap.js.map +0 -1
- package/dist/csm.cjs +0 -99
- package/dist/csm.cjs.map +0 -1
- package/dist/csm.d.cts +0 -391
- package/dist/index.browser.cjs +0 -3106
- package/dist/index.browser.cjs.map +0 -1
- package/dist/index.browser.d.ts +0 -6802
- package/dist/index.browser.js +0 -3082
- package/dist/index.browser.js.map +0 -1
- package/dist/index.cjs +0 -2946
- package/dist/index.cjs.map +0 -1
- package/dist/media-library.cjs +0 -20
- package/dist/media-library.cjs.map +0 -1
- package/dist/media-library.d.cts +0 -117
- package/dist/stega.browser.cjs +0 -29
- package/dist/stega.browser.cjs.map +0 -1
- package/dist/stega.browser.d.cts +0 -7200
- package/dist/stega.browser.d.ts +0 -7200
- package/dist/stega.browser.js +0 -24
- package/dist/stega.browser.js.map +0 -1
- package/dist/stega.cjs +0 -29
- package/dist/stega.cjs.map +0 -1
- package/dist/stega.d.cts +0 -7200
- package/src/data/eventsourcePolyfill.ts +0 -7
- package/umd/.gitkeep +0 -1
- package/umd/sanityClient.js +0 -6543
- package/umd/sanityClient.min.js +0 -6
package/src/data/eventsource.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
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'
|
|
15
17
|
/**
|
|
16
18
|
* HTTP status code of the rejected connection attempt, if known.
|
|
17
|
-
* Only set when the EventSource implementation exposes it — the
|
|
18
|
-
*
|
|
19
|
+
* Only set when the EventSource implementation exposes it — the `eventsource`
|
|
20
|
+
* package used by the client does (as `code` on its error events), while
|
|
19
21
|
* native EventSource implementations (browser and Node.js) do not.
|
|
20
22
|
*/
|
|
21
23
|
readonly status?: number
|
|
@@ -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,19 +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
|
|
154
|
-
? new MessageParseError('Unable to parse EventSource error message', {
|
|
155
|
-
|
|
161
|
+
parseError || !event
|
|
162
|
+
? new MessageParseError('Unable to parse EventSource error message', {
|
|
163
|
+
cause: parseError,
|
|
164
|
+
})
|
|
165
|
+
: new MessageError(
|
|
166
|
+
isRecord(event.data) && typeof event.data.message === 'string'
|
|
167
|
+
? event.data.message
|
|
168
|
+
: '',
|
|
169
|
+
event,
|
|
170
|
+
),
|
|
156
171
|
)
|
|
157
172
|
return
|
|
158
173
|
}
|
|
@@ -161,34 +176,41 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
161
176
|
// automatically, but in some cases (like when a laptop lid is closed), it will trigger onError
|
|
162
177
|
// if it can't reconnect.
|
|
163
178
|
// see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
|
|
164
|
-
// The
|
|
165
|
-
// (native EventSource implementations
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
// The `eventsource` package exposes the HTTP status of a rejected connection as
|
|
180
|
+
// `code` on the error event (native EventSource implementations expose nothing).
|
|
181
|
+
// A status means the server rejected the connection attempt, so error out
|
|
182
|
+
// regardless of readyState — implementations disagree on whether the connection
|
|
183
|
+
// closes before or after the error event is dispatched — and let
|
|
184
|
+
// `reconnectOnConnectionFailure` classify it (4xx fatal, otherwise retried).
|
|
185
|
+
if (evt.code !== undefined) {
|
|
186
|
+
observer.error(
|
|
187
|
+
new ConnectionFailedError('EventSource connection failed', {status: evt.code}),
|
|
188
|
+
)
|
|
173
189
|
return
|
|
174
190
|
}
|
|
175
191
|
|
|
176
192
|
if (es.readyState === es.CLOSED) {
|
|
177
193
|
// In these cases we'll signal to consumers (via the error path) that a retry/reconnect is needed.
|
|
178
194
|
observer.error(new ConnectionFailedError('EventSource connection failed'))
|
|
179
|
-
} else
|
|
180
|
-
|
|
195
|
+
} else {
|
|
196
|
+
const type = 'reconnect'
|
|
197
|
+
if (isRequestedEvent(type)) {
|
|
198
|
+
observer.next({type})
|
|
199
|
+
}
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
|
|
184
203
|
function onOpen() {
|
|
185
204
|
// The open event of the EventSource API is fired when a connection with an event source is opened.
|
|
186
|
-
|
|
205
|
+
const type = 'open'
|
|
206
|
+
if (isRequestedEvent(type)) {
|
|
207
|
+
observer.next({type})
|
|
208
|
+
}
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
function onMessage(message: MessageEvent) {
|
|
190
212
|
const [parseError, event] = parseEvent(message)
|
|
191
|
-
if (parseError) {
|
|
213
|
+
if (parseError || !event) {
|
|
192
214
|
observer.error(
|
|
193
215
|
new MessageParseError('Unable to parse EventSource message', {cause: parseError}),
|
|
194
216
|
)
|
|
@@ -209,17 +231,25 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
209
231
|
observer.error(
|
|
210
232
|
new DisconnectError(
|
|
211
233
|
`Server disconnected client: ${
|
|
212
|
-
(event.data
|
|
234
|
+
(isRecord(event.data) &&
|
|
235
|
+
typeof event.data.reason === 'string' &&
|
|
236
|
+
event.data.reason) ||
|
|
237
|
+
'unknown error'
|
|
213
238
|
}`,
|
|
214
239
|
),
|
|
215
240
|
)
|
|
216
241
|
return
|
|
217
242
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
es.addEventListener('error', onError)
|
|
@@ -248,7 +278,7 @@ function connectWithESInstance<EventTypeName extends string>(
|
|
|
248
278
|
|
|
249
279
|
function parseEvent(
|
|
250
280
|
message: MessageEvent,
|
|
251
|
-
): [null, {type: string; id: string; data?: unknown}] | [
|
|
281
|
+
): [null, {type: string; id: string; data?: unknown}] | [unknown, null] {
|
|
252
282
|
try {
|
|
253
283
|
const data = typeof message.data === 'string' && JSON.parse(message.data)
|
|
254
284
|
return [
|
|
@@ -260,23 +290,26 @@ function parseEvent(
|
|
|
260
290
|
},
|
|
261
291
|
]
|
|
262
292
|
} catch (err) {
|
|
263
|
-
return [err
|
|
293
|
+
return [err, null]
|
|
264
294
|
}
|
|
265
295
|
}
|
|
266
296
|
|
|
267
|
-
function extractErrorMessage(err:
|
|
268
|
-
const error = err.error
|
|
297
|
+
function extractErrorMessage(err: unknown, tag?: string | null): string {
|
|
298
|
+
const error = isRecord(err) ? err.error : undefined
|
|
269
299
|
|
|
270
300
|
if (!error) {
|
|
271
|
-
|
|
301
|
+
const message = isRecord(err) ? err.message : undefined
|
|
302
|
+
return (typeof message === 'string' && message) || 'Unknown listener error'
|
|
272
303
|
}
|
|
273
304
|
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
305
|
+
if (isRecord(error)) {
|
|
306
|
+
if (isQueryParseError(error)) {
|
|
307
|
+
return formatQueryParseError(error, tag)
|
|
308
|
+
}
|
|
277
309
|
|
|
278
|
-
|
|
279
|
-
|
|
310
|
+
if (typeof error.description === 'string') {
|
|
311
|
+
return error.description
|
|
312
|
+
}
|
|
280
313
|
}
|
|
281
314
|
|
|
282
315
|
return typeof error === 'string' ? error : JSON.stringify(error, null, 2)
|
package/src/data/listen.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {EventSource} from 'eventsource'
|
|
2
|
+
import {Observable, throwError} from 'rxjs'
|
|
2
3
|
import {filter, map} from 'rxjs/operators'
|
|
3
4
|
|
|
4
5
|
import type {ObservableSanityClient, SanityClient} from '../SanityClient'
|
|
@@ -22,8 +23,8 @@ import {pick} from '../util/pick'
|
|
|
22
23
|
import {_getDataUrl} from './dataMethods'
|
|
23
24
|
import {encodeQueryString} from './encodeQueryString'
|
|
24
25
|
import {connectEventSource} from './eventsource'
|
|
25
|
-
import {eventSourcePolyfill} from './eventsourcePolyfill'
|
|
26
26
|
import {reconnectOnConnectionFailure} from './reconnectOnConnectionFailure'
|
|
27
|
+
import {resolveEventSourceFetch} from './resolveEventSourceFetch'
|
|
27
28
|
|
|
28
29
|
// Limit is 16K for a _request_, eg including headers. Have to account for an
|
|
29
30
|
// unknown range of headers, but an average EventSource request from Chrome seems
|
|
@@ -57,7 +58,8 @@ const defaultOptions = {
|
|
|
57
58
|
export type MapListenEventNamesToListenEvents<
|
|
58
59
|
R extends Record<string, Any> = Record<string, Any>,
|
|
59
60
|
Events extends (ResumableListenEventNames | ListenEventName)[] = (
|
|
60
|
-
|
|
61
|
+
| ResumableListenEventNames
|
|
62
|
+
| ListenEventName
|
|
61
63
|
)[],
|
|
62
64
|
> = Events extends (infer E)[]
|
|
63
65
|
? E extends 'welcome'
|
|
@@ -134,7 +136,8 @@ export function _listen<
|
|
|
134
136
|
params?: ListenParams,
|
|
135
137
|
opts: Opts = {} as Opts,
|
|
136
138
|
): Observable<ListenEventFromOptions<R, Opts>> {
|
|
137
|
-
const
|
|
139
|
+
const config = this.config()
|
|
140
|
+
const {url, token, withCredentials, requestTagPrefix, headers: configHeaders} = config
|
|
138
141
|
const tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join('.') : opts.tag
|
|
139
142
|
const options = {...defaults(opts, defaultOptions), tag}
|
|
140
143
|
const listenOpts = pick(options, possibleOptions)
|
|
@@ -147,29 +150,21 @@ export function _listen<
|
|
|
147
150
|
|
|
148
151
|
const listenFor = (options.events ? options.events : ['mutation']) satisfies Opts['events']
|
|
149
152
|
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
+
const headers: Record<string, string> = {}
|
|
154
|
+
if (token) {
|
|
155
|
+
headers.Authorization = `Bearer ${token}`
|
|
153
156
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
esOptions.headers = {}
|
|
157
|
-
|
|
158
|
-
if (token) {
|
|
159
|
-
esOptions.headers.Authorization = `Bearer ${token}`
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (configHeaders) {
|
|
163
|
-
Object.assign(esOptions.headers, configHeaders)
|
|
164
|
-
}
|
|
157
|
+
if (configHeaders) {
|
|
158
|
+
Object.assign(headers, configHeaders)
|
|
165
159
|
}
|
|
166
160
|
|
|
167
161
|
const initEventSource = () =>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
162
|
+
new EventSource(uri, {
|
|
163
|
+
fetch: resolveEventSourceFetch(config, {
|
|
164
|
+
headers: Object.keys(headers).length ? headers : undefined,
|
|
165
|
+
withCredentials,
|
|
166
|
+
}),
|
|
167
|
+
})
|
|
173
168
|
|
|
174
169
|
return connectEventSource(initEventSource, listenFor).pipe(
|
|
175
170
|
reconnectOnConnectionFailure(),
|
package/src/data/live.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import {EventSource} from 'eventsource'
|
|
2
|
+
import type {FetchFunction} from 'get-it'
|
|
1
3
|
import {catchError, mergeMap, Observable, of, throwError} from 'rxjs'
|
|
2
4
|
import {finalize, map} from 'rxjs/operators'
|
|
3
5
|
|
|
4
6
|
import {CorsOriginError} from '../http/errors'
|
|
5
7
|
import type {ObservableSanityClient, SanityClient} from '../SanityClient'
|
|
6
8
|
import type {
|
|
9
|
+
InitializedClientConfig,
|
|
7
10
|
LiveEvent,
|
|
8
11
|
LiveEventGoAway,
|
|
9
12
|
LiveEventMessage,
|
|
@@ -12,11 +15,12 @@ import type {
|
|
|
12
15
|
LiveEventWelcome,
|
|
13
16
|
SyncTag,
|
|
14
17
|
} from '../types'
|
|
18
|
+
import {isRecord} from '../util/isRecord'
|
|
15
19
|
import {shareReplayLatest} from '../util/shareReplayLatest'
|
|
16
20
|
import {_getDataUrl} from './dataMethods'
|
|
17
21
|
import {connectEventSource} from './eventsource'
|
|
18
|
-
import {eventSourcePolyfill} from './eventsourcePolyfill'
|
|
19
22
|
import {reconnectOnConnectionFailure} from './reconnectOnConnectionFailure'
|
|
23
|
+
import {pickBaseFetch, resolveEventSourceFetch} from './resolveEventSourceFetch'
|
|
20
24
|
|
|
21
25
|
const requiredApiVersion = '2021-03-25'
|
|
22
26
|
|
|
@@ -50,6 +54,7 @@ export class LiveClient {
|
|
|
50
54
|
*/
|
|
51
55
|
waitFor?: 'function'
|
|
52
56
|
} = {}): Observable<LiveEvent> {
|
|
57
|
+
const config = this.#client.config()
|
|
53
58
|
const {
|
|
54
59
|
projectId,
|
|
55
60
|
apiVersion: _apiVersion,
|
|
@@ -57,7 +62,7 @@ export class LiveClient {
|
|
|
57
62
|
withCredentials,
|
|
58
63
|
requestTagPrefix,
|
|
59
64
|
headers: configHeaders,
|
|
60
|
-
} =
|
|
65
|
+
} = config
|
|
61
66
|
const apiVersion = _apiVersion.replace(/^v/, '')
|
|
62
67
|
if (apiVersion !== 'X' && apiVersion < requiredApiVersion) {
|
|
63
68
|
throw new Error(
|
|
@@ -83,36 +88,39 @@ export class LiveClient {
|
|
|
83
88
|
if (waitFor) {
|
|
84
89
|
url.searchParams.set('waitFor', waitFor)
|
|
85
90
|
}
|
|
86
|
-
const
|
|
87
|
-
if (includeDrafts &&
|
|
88
|
-
|
|
91
|
+
const eventSourceHeaders: Record<string, string> = {}
|
|
92
|
+
if (includeDrafts && token) {
|
|
93
|
+
eventSourceHeaders.Authorization = `Bearer ${token}`
|
|
89
94
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
esOptions.headers = {}
|
|
93
|
-
|
|
94
|
-
if (includeDrafts && token) {
|
|
95
|
-
esOptions.headers.Authorization = `Bearer ${token}`
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (configHeaders) {
|
|
99
|
-
Object.assign(esOptions.headers, configHeaders)
|
|
100
|
-
}
|
|
95
|
+
if (configHeaders) {
|
|
96
|
+
Object.assign(eventSourceHeaders, configHeaders)
|
|
101
97
|
}
|
|
98
|
+
const eventSourceWithCredentials = Boolean(includeDrafts && withCredentials)
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
let transportCache = eventsCache.get(config.resolveFetch)
|
|
101
|
+
if (!transportCache) {
|
|
102
|
+
transportCache = new Map()
|
|
103
|
+
eventsCache.set(config.resolveFetch, transportCache)
|
|
104
|
+
}
|
|
105
|
+
const cacheKey = JSON.stringify([
|
|
106
|
+
url.href,
|
|
107
|
+
typeof config.proxy === 'string' ? config.proxy : null,
|
|
108
|
+
eventSourceHeaders,
|
|
109
|
+
eventSourceWithCredentials,
|
|
110
|
+
])
|
|
111
|
+
const existing = transportCache.get(cacheKey)
|
|
105
112
|
|
|
106
113
|
if (existing) {
|
|
107
114
|
return existing
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
const initEventSource = () =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
new EventSource(url.href, {
|
|
119
|
+
fetch: resolveEventSourceFetch(config, {
|
|
120
|
+
headers: Object.keys(eventSourceHeaders).length ? eventSourceHeaders : undefined,
|
|
121
|
+
withCredentials: eventSourceWithCredentials,
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
116
124
|
|
|
117
125
|
const events = connectEventSource(initEventSource, [
|
|
118
126
|
'message',
|
|
@@ -125,7 +133,8 @@ export class LiveClient {
|
|
|
125
133
|
const checkCors = checkCorsObservable(
|
|
126
134
|
new URL(this.#client.getUrl('/check/cors', false)),
|
|
127
135
|
projectId,
|
|
128
|
-
|
|
136
|
+
eventSourceWithCredentials,
|
|
137
|
+
pickBaseFetch(config),
|
|
129
138
|
)
|
|
130
139
|
|
|
131
140
|
const observable = events
|
|
@@ -162,12 +171,15 @@ export class LiveClient {
|
|
|
162
171
|
}),
|
|
163
172
|
)
|
|
164
173
|
.pipe(
|
|
165
|
-
finalize(() =>
|
|
174
|
+
finalize(() => {
|
|
175
|
+
transportCache.delete(cacheKey)
|
|
176
|
+
if (transportCache.size === 0) eventsCache.delete(config.resolveFetch)
|
|
177
|
+
}),
|
|
166
178
|
shareReplayLatest({
|
|
167
179
|
predicate: (event) => event.type === 'welcome',
|
|
168
180
|
}),
|
|
169
181
|
)
|
|
170
|
-
|
|
182
|
+
transportCache.set(cacheKey, observable)
|
|
171
183
|
return observable
|
|
172
184
|
}
|
|
173
185
|
}
|
|
@@ -212,35 +224,40 @@ function checkCorsObservable(
|
|
|
212
224
|
url: URL,
|
|
213
225
|
projectId: string | undefined,
|
|
214
226
|
requireCredentials: boolean,
|
|
227
|
+
fetcher: FetchFunction,
|
|
215
228
|
): Observable<void> {
|
|
216
229
|
return new Observable<void>((observer) => {
|
|
217
230
|
const controller = new AbortController()
|
|
218
231
|
const {signal} = controller
|
|
219
|
-
|
|
232
|
+
fetcher(url.href, {method: 'GET', credentials: 'omit', signal})
|
|
220
233
|
.then((response) => {
|
|
221
234
|
// Aborted or non-2xx: not a confirmed CORS rejection. Fall through with
|
|
222
235
|
// an undefined body so the next step takes the silent-completion path.
|
|
223
|
-
if (signal.aborted || !response.ok) return
|
|
224
|
-
return response.
|
|
225
|
-
result?: {allowed?: boolean; withCredentials?: boolean}
|
|
226
|
-
}>
|
|
236
|
+
if (signal.aborted || !response.ok) return undefined
|
|
237
|
+
return response.text()
|
|
227
238
|
})
|
|
228
|
-
.then((
|
|
239
|
+
.then((text) => {
|
|
229
240
|
if (signal.aborted) return
|
|
241
|
+
// `get-it`'s `FetchResponse` only guarantees `.text()`/`.arrayBuffer()`,
|
|
242
|
+
// not `.json()`, so the body is parsed by hand here. An empty/aborted
|
|
243
|
+
// fall-through (`text === undefined`) and malformed JSON both leave
|
|
244
|
+
// `result` unresolved, taking the same "no signal either way" path.
|
|
245
|
+
const parsed: unknown = text === undefined ? undefined : JSON.parse(text)
|
|
246
|
+
const result = isRecord(parsed) ? parsed.result : undefined
|
|
230
247
|
// Check the credentialed case first: if the EventSource was about to
|
|
231
248
|
// send credentials but the project's CORS config doesn't permit them,
|
|
232
249
|
// the credentialed request would fail with a missing
|
|
233
250
|
// `access-control-allow-credentials` header. Surface this as a CORS
|
|
234
251
|
// rejection with `credentials: true` so the deep-link pre-selects the
|
|
235
252
|
// "Allow credentials" toggle.
|
|
236
|
-
if (requireCredentials &&
|
|
253
|
+
if (requireCredentials && isRecord(result) && result.withCredentials === false) {
|
|
237
254
|
observer.error(new CorsOriginError({projectId, credentials: true}))
|
|
238
255
|
return
|
|
239
256
|
}
|
|
240
257
|
// Generic case: the server actively rejected this origin. Propagate
|
|
241
258
|
// `credentials: requireCredentials` so the deep-link still pre-selects
|
|
242
259
|
// credentials when the caller needed them.
|
|
243
|
-
if (
|
|
260
|
+
if (isRecord(result) && result.allowed === false) {
|
|
244
261
|
observer.error(new CorsOriginError({projectId, credentials: requireCredentials}))
|
|
245
262
|
return
|
|
246
263
|
}
|
|
@@ -262,4 +279,12 @@ function checkCorsObservable(
|
|
|
262
279
|
})
|
|
263
280
|
}
|
|
264
281
|
|
|
265
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Cached observables capture their transport (`initEventSource` closes over
|
|
284
|
+
* `config.resolveFetch` and `config.proxy`), so the cache is scoped per
|
|
285
|
+
* resolver — `undefined` covers the `globalThis.fetch` fallback.
|
|
286
|
+
*/
|
|
287
|
+
const eventsCache = new Map<
|
|
288
|
+
InitializedClientConfig['resolveFetch'],
|
|
289
|
+
Map<string, Observable<LiveEvent>>
|
|
290
|
+
>()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {EventSourceFetchInit, FetchLikeResponse} from 'eventsource'
|
|
2
|
+
import type {FetchFunction, FetchInit} from 'get-it'
|
|
3
|
+
|
|
4
|
+
import type {InitializedClientConfig} from '../types'
|
|
5
|
+
|
|
6
|
+
/** @internal */
|
|
7
|
+
export interface EventSourceFetchOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Headers that must be merged into every EventSource request. Used for
|
|
10
|
+
* the `Authorization` token, custom `headers` from the client config,
|
|
11
|
+
* etc. — things the native EventSource API has no equivalent for.
|
|
12
|
+
*/
|
|
13
|
+
headers?: Record<string, string>
|
|
14
|
+
/**
|
|
15
|
+
* If the client was configured with `withCredentials: true`, the
|
|
16
|
+
* resolved fetch forwards `credentials: 'include'` so the browser
|
|
17
|
+
* attaches cookies to the SSE request.
|
|
18
|
+
*/
|
|
19
|
+
withCredentials?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a `fetch` implementation suitable for the `eventsource` package's
|
|
24
|
+
* `fetch` option. Routes the EventSource connection through the same
|
|
25
|
+
* transport layer the rest of the client uses, so things like the
|
|
26
|
+
* test-fetch override, the per-request `proxy` config, and `HTTPS_PROXY`
|
|
27
|
+
* env-var support apply to SSE too.
|
|
28
|
+
*
|
|
29
|
+
* Resolution order on each request:
|
|
30
|
+
*
|
|
31
|
+
* 1. `config.resolveFetch(config.proxy)` if set — the client's fetch
|
|
32
|
+
* resolver, so SSE uses the same transport as regular requests:
|
|
33
|
+
* custom fetch variants (incl. the test suite's injected mock),
|
|
34
|
+
* undici configuration, an explicit `proxy` config, and env-proxy
|
|
35
|
+
* support all apply to SSE too. The Node entry supplies get-it's
|
|
36
|
+
* undici-backed fetch (threading the resolver through the env
|
|
37
|
+
* instead of importing `get-it/node` directly keeps `undici` out of
|
|
38
|
+
* the browser bundle); the browser entry leaves it unset.
|
|
39
|
+
* 2. `globalThis.fetch`. Note that Node's global fetch does NOT read
|
|
40
|
+
* proxy env vars (that is opt-in via `NODE_USE_ENV_PROXY`), which
|
|
41
|
+
* is one of the reasons step 1 exists.
|
|
42
|
+
*
|
|
43
|
+
* The returned fetch always merges `options.headers` into the outgoing
|
|
44
|
+
* request, regardless of which underlying fetch was picked.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function resolveEventSourceFetch(
|
|
49
|
+
config: InitializedClientConfig,
|
|
50
|
+
options: EventSourceFetchOptions = {},
|
|
51
|
+
): EventSourceFetch {
|
|
52
|
+
const extraHeaders = options.headers
|
|
53
|
+
const credentials: FetchInit['credentials'] = options.withCredentials ? 'include' : undefined
|
|
54
|
+
|
|
55
|
+
return function eventSourceFetch(url, init) {
|
|
56
|
+
const baseFetch = pickBaseFetch(config)
|
|
57
|
+
|
|
58
|
+
// Extra `EventSourceFetchInit` fields get-it's `FetchInit` doesn't
|
|
59
|
+
// declare (`mode`, `cache`) survive the spread and reach whichever
|
|
60
|
+
// fetch implementation is effective.
|
|
61
|
+
const mergedInit: FetchInit = {...init}
|
|
62
|
+
if (extraHeaders) {
|
|
63
|
+
const headers = new Headers(init?.headers)
|
|
64
|
+
for (const [key, value] of Object.entries(extraHeaders)) {
|
|
65
|
+
headers.set(key, value)
|
|
66
|
+
}
|
|
67
|
+
mergedInit.headers = headers
|
|
68
|
+
}
|
|
69
|
+
if (credentials !== undefined) {
|
|
70
|
+
mergedInit.credentials = credentials
|
|
71
|
+
}
|
|
72
|
+
// get-it's `FetchResponse` is a structural superset of the package's
|
|
73
|
+
// `FetchLikeResponse`, so it can be handed over as-is.
|
|
74
|
+
return baseFetch(typeof url === 'string' ? url : url.href, mergedInit)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The fetch shape handed to the `eventsource` package: accepts what its
|
|
80
|
+
* `FetchLike` passes in, requires only what get-it's `FetchFunction`
|
|
81
|
+
* guarantees back — the full `typeof fetch` contract is not needed anywhere
|
|
82
|
+
* in this chain.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
export type EventSourceFetch = (
|
|
87
|
+
url: string | URL,
|
|
88
|
+
init?: EventSourceFetchInit,
|
|
89
|
+
) => Promise<FetchLikeResponse>
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The fetch the client's own transport resolves for this config: the
|
|
93
|
+
* configured `resolveFetch` (honouring an explicit `proxy`) if present,
|
|
94
|
+
* otherwise the global fetch. Shared by the EventSource connection and the
|
|
95
|
+
* `/check/cors` probe so both resolve identically.
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
export function pickBaseFetch(config: InitializedClientConfig): FetchFunction {
|
|
100
|
+
if (config.resolveFetch) {
|
|
101
|
+
return config.resolveFetch(typeof config.proxy === 'string' ? config.proxy : undefined)
|
|
102
|
+
}
|
|
103
|
+
return globalThis.fetch.bind(globalThis)
|
|
104
|
+
}
|