@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.
Files changed (58) hide show
  1. package/README.md +265 -0
  2. package/dist/{browserUpload-CQgx9YYo.js → browserUpload-2tz6Sdqp.js} +4 -3
  3. package/dist/browserUpload-2tz6Sdqp.js.map +1 -0
  4. package/dist/{browserUpload-icWlVP15.js → browserUpload-CwpNx7Vl.js} +4 -3
  5. package/dist/browserUpload-CwpNx7Vl.js.map +1 -0
  6. package/dist/{config-a8VajuEY.js → config-3wiPP-sZ.js} +2 -2
  7. package/dist/config-3wiPP-sZ.js.map +1 -0
  8. package/dist/csm.js +2 -2
  9. package/dist/csm.js.map +1 -1
  10. package/dist/index.d.ts +17 -11
  11. package/dist/index.js +610 -136
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.node.d.ts +918 -26
  14. package/dist/index.node.js +559 -66
  15. package/dist/index.node.js.map +1 -1
  16. package/dist/media-library.d.ts +1 -1
  17. package/dist/{request-CJxcN16k.js → request-BhMuKj0D.js} +10 -9
  18. package/dist/request-BhMuKj0D.js.map +1 -0
  19. package/dist/{request-k7VS_NnC.js → request-SnMg7nUX.js} +10 -9
  20. package/dist/request-SnMg7nUX.js.map +1 -0
  21. package/dist/{resolveEditInfo-sq7yF78q.js → resolveEditInfo-Cz-smq3a.js} +17 -3
  22. package/dist/resolveEditInfo-Cz-smq3a.js.map +1 -0
  23. package/dist/stega.js +1 -1
  24. package/dist/{stegaEncodeSourceMap-DkoIlutY.js → stegaEncodeSourceMap-DbM2fTN4.js} +8 -2
  25. package/dist/stegaEncodeSourceMap-DbM2fTN4.js.map +1 -0
  26. package/dist/{stegaEncodeSourceMap-B2fGArSf.js → stegaEncodeSourceMap-YR3NQ3iz.js} +2 -2
  27. package/dist/{stegaEncodeSourceMap-B2fGArSf.js.map → stegaEncodeSourceMap-YR3NQ3iz.js.map} +1 -1
  28. package/dist/{types-CUxZSgB2.d.ts → types-nJhm5Nyq.d.ts} +910 -24
  29. package/package.json +26 -11
  30. package/src/SanityClient.ts +39 -20
  31. package/src/assets/AssetsClient.ts +54 -5
  32. package/src/collaboration/CollaborationCommentsClient.ts +387 -0
  33. package/src/collaboration/comments.ts +313 -0
  34. package/src/collaboration/types.ts +252 -0
  35. package/src/csm/applySourceDocuments.ts +2 -4
  36. package/src/csm/draftUtils.ts +23 -4
  37. package/src/data/dataMethods.ts +9 -20
  38. package/src/data/eventsource.ts +71 -41
  39. package/src/data/listen.ts +20 -5
  40. package/src/data/live.ts +17 -9
  41. package/src/data/resolveEventSourceFetch.ts +9 -1
  42. package/src/defineCreateClient.ts +5 -1
  43. package/src/functions/FunctionsClient.ts +66 -0
  44. package/src/functions/invoke.ts +176 -0
  45. package/src/http/browserUpload.ts +1 -0
  46. package/src/http/errors.ts +2 -1
  47. package/src/http/request.ts +8 -14
  48. package/src/mediaLibrary/MediaLibraryVideoClient.ts +1 -1
  49. package/src/types.ts +420 -4
  50. package/src/validators.ts +1 -1
  51. package/src/warnings.ts +7 -1
  52. package/dist/browserUpload-CQgx9YYo.js.map +0 -1
  53. package/dist/browserUpload-icWlVP15.js.map +0 -1
  54. package/dist/config-a8VajuEY.js.map +0 -1
  55. package/dist/request-CJxcN16k.js.map +0 -1
  56. package/dist/request-k7VS_NnC.js.map +0 -1
  57. package/dist/resolveEditInfo-sq7yF78q.js.map +0 -1
  58. 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 as Path)
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 Any, {
68
+ : updateFn<Result[keyof Result]>(changedValue as Result[keyof Result], {
71
69
  cachedDocument,
72
70
  previousValue: value as Result[keyof Result],
73
71
  sourceDocument,
@@ -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) as DraftId
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) as PublishedId as PublishedId
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) as PublishedId
89
+ return asPublishedId(id.slice(DRAFTS_PREFIX.length))
90
+ }
91
+
92
+ if (isPublishedId(id)) {
93
+ return id
77
94
  }
78
95
 
79
- return id as PublishedId
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
  }
@@ -73,7 +73,13 @@ const indexBy = (docs: Any[], attr: Any) =>
73
73
  return indexed
74
74
  }, Object.create(null))
75
75
 
76
- const getQuerySizeLimit = 11264
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
- export function _observe<R>(
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
  /**
@@ -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 {type Any} from '../types'
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
- * This is a rare situation, but may occur if the API knows reconnect attempts will fail,
32
- * eg in the case of a deleted dataset, a blocked project or similar events.
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<typeof globalThis.EventSource>
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))) as Observable<
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
- const emitOpen = (events as string[]).includes('open')
144
- const emitReconnect = (events as string[]).includes('reconnect')
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: MessageEvent | Event) {
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 as MessageEvent)
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((event.data as {message: string}).message, event),
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
- const rawStatus = (evt as {code?: unknown}).code
173
- const status = typeof rawStatus === 'number' ? rawStatus : undefined
174
- if (status !== undefined) {
175
- observer.error(new ConnectionFailedError('EventSource connection failed', {status}))
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 if (emitReconnect) {
183
- observer.next({type: 'reconnect' as EventTypeName})
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
- observer.next({type: 'open' as EventTypeName})
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 as {reason?: string})?.reason || 'unknown error'
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
- observer.next({
222
- type: message.type as EventTypeName,
223
- id: message.lastEventId,
224
- ...(event.data ? {data: event.data} : {}),
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}] | [Error, null] {
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 as Error, null]
293
+ return [err, null]
267
294
  }
268
295
  }
269
296
 
270
- function extractErrorMessage(err: Any, tag?: string | null) {
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
- return err.message || 'Unknown listener error'
301
+ const message = isRecord(err) ? err.message : undefined
302
+ return (typeof message === 'string' && message) || 'Unknown listener error'
275
303
  }
276
304
 
277
- if (isQueryParseError(error)) {
278
- return formatQueryParseError(error, tag)
279
- }
305
+ if (isRecord(error)) {
306
+ if (isQueryParseError(error)) {
307
+ return formatQueryParseError(error, tag)
308
+ }
280
309
 
281
- if (error.description) {
282
- return error.description
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)
@@ -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
- const MAX_URL_LENGTH = 16000 - 1200
32
+ /** @internal */
33
+ export const MAX_URL_LENGTH = 16000 - 1200
33
34
 
34
- const possibleOptions = [
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
- const defaultOptions = {
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, token, withCredentials, requestTagPrefix, headers: configHeaders} = config
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<ListenEventFromOptions<R, Opts>>
191
+ ) as Observable<TEvent>
177
192
  }