@sanity/client 8.0.0 → 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.
Files changed (54) hide show
  1. package/README.md +94 -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 +247 -120
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.node.d.ts +270 -26
  14. package/dist/index.node.js +183 -50
  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-BODIEY7F.d.ts} +262 -24
  29. package/package.json +26 -11
  30. package/src/SanityClient.ts +19 -20
  31. package/src/assets/AssetsClient.ts +54 -5
  32. package/src/csm/applySourceDocuments.ts +2 -4
  33. package/src/csm/draftUtils.ts +23 -4
  34. package/src/data/dataMethods.ts +2 -19
  35. package/src/data/eventsource.ts +71 -41
  36. package/src/data/live.ts +17 -9
  37. package/src/data/resolveEventSourceFetch.ts +9 -1
  38. package/src/defineCreateClient.ts +5 -1
  39. package/src/functions/FunctionsClient.ts +66 -0
  40. package/src/functions/invoke.ts +176 -0
  41. package/src/http/browserUpload.ts +1 -0
  42. package/src/http/errors.ts +2 -1
  43. package/src/http/request.ts +8 -14
  44. package/src/mediaLibrary/MediaLibraryVideoClient.ts +1 -1
  45. package/src/types.ts +186 -4
  46. package/src/validators.ts +1 -1
  47. package/src/warnings.ts +7 -1
  48. package/dist/browserUpload-CQgx9YYo.js.map +0 -1
  49. package/dist/browserUpload-icWlVP15.js.map +0 -1
  50. package/dist/config-a8VajuEY.js.map +0 -1
  51. package/dist/request-CJxcN16k.js.map +0 -1
  52. package/dist/request-k7VS_NnC.js.map +0 -1
  53. package/dist/resolveEditInfo-sq7yF78q.js.map +0 -1
  54. package/dist/stegaEncodeSourceMap-DkoIlutY.js.map +0 -1
@@ -10,6 +10,7 @@ import {LiveClient} from './data/live'
10
10
  import {ObservablePatch, Patch} from './data/patch'
11
11
  import {ObservableTransaction, Transaction} from './data/transaction'
12
12
  import {DatasetsClient, ObservableDatasetsClient} from './datasets/DatasetsClient'
13
+ import {FunctionsClient, ObservableFunctionsClient} from './functions/FunctionsClient'
13
14
  import {
14
15
  MediaLibraryVideoClient,
15
16
  ObservableMediaLibraryVideoClient,
@@ -81,6 +82,7 @@ export class ObservableSanityClient {
81
82
  agent: {
82
83
  action: ObservableAgentsActionClient
83
84
  }
85
+ functions: ObservableFunctionsClient
84
86
  releases: ObservableReleasesClient
85
87
 
86
88
  /**
@@ -110,6 +112,7 @@ export class ObservableSanityClient {
110
112
  this.agent = {
111
113
  action: new ObservableAgentsActionClient(this, this.#httpRequest),
112
114
  }
115
+ this.functions = new ObservableFunctionsClient(this, this.#httpRequest)
113
116
  this.releases = new ObservableReleasesClient(this, this.#httpRequest)
114
117
  }
115
118
 
@@ -707,15 +710,12 @@ export class ObservableSanityClient {
707
710
  delete(id: string, options: AllDocumentIdsMutationOptions): Observable<MultipleMutationResult>
708
711
  /**
709
712
  * Deletes a document with the given document ID.
710
- * Returns an observable that resolves to the deleted document.
713
+ * Returns an observable that resolves to a mutation result object containing the deleted document ID.
711
714
  *
712
715
  * @param id - Document ID to delete
713
716
  * @param options - Options for the mutation
714
717
  */
715
- delete<R extends Record<string, Any> = Record<string, Any>>(
716
- id: string,
717
- options?: BaseMutationOptions,
718
- ): Observable<SanityDocument<R>>
718
+ delete(id: string, options?: BaseMutationOptions): Observable<MultipleMutationResult>
719
719
  /**
720
720
  * Deletes one or more documents matching the given query or document ID.
721
721
  * Returns an observable that resolves to first deleted document.
@@ -762,15 +762,15 @@ export class ObservableSanityClient {
762
762
  ): Observable<MultipleMutationResult>
763
763
  /**
764
764
  * Deletes one or more documents matching the given query or document ID.
765
- * Returns an observable that resolves to first deleted document.
765
+ * Returns an observable that resolves to a mutation result object containing the document IDs that were deleted.
766
766
  *
767
767
  * @param selection - An object with either an `id` or `query` key defining what to delete
768
768
  * @param options - Options for the mutation
769
769
  */
770
- delete<R extends Record<string, Any> = Record<string, Any>>(
770
+ delete(
771
771
  selection: MutationSelection,
772
772
  options?: BaseMutationOptions,
773
- ): Observable<SanityDocument<R>>
773
+ ): Observable<MultipleMutationResult>
774
774
  delete<R extends Record<string, Any> = Record<string, Any>>(
775
775
  selection: string | MutationSelection,
776
776
  options?:
@@ -1020,7 +1020,7 @@ export class ObservableSanityClient {
1020
1020
  ): Observable<MultipleMutationResult>
1021
1021
  /**
1022
1022
  * Perform mutation operations against the configured dataset
1023
- * Returns an observable that resolves to the first mutated document.
1023
+ * Returns an observable that resolves to a mutation result object containing the mutated document IDs.
1024
1024
  *
1025
1025
  * @param operations - Mutation operations to execute
1026
1026
  * @param options - Mutation options
@@ -1028,7 +1028,7 @@ export class ObservableSanityClient {
1028
1028
  mutate<R extends Record<string, Any> = Record<string, Any>>(
1029
1029
  operations: Mutation<R>[] | ObservablePatch | ObservableTransaction,
1030
1030
  options?: BaseMutationOptions,
1031
- ): Observable<SanityDocument<R>>
1031
+ ): Observable<MultipleMutationResult>
1032
1032
  mutate<R extends Record<string, Any> = Record<string, Any>>(
1033
1033
  operations: Mutation<R>[] | ObservablePatch | ObservableTransaction,
1034
1034
  options?:
@@ -1148,6 +1148,7 @@ export class SanityClient {
1148
1148
  agent: {
1149
1149
  action: AgentActionsClient
1150
1150
  }
1151
+ functions: FunctionsClient
1151
1152
  releases: ReleasesClient
1152
1153
 
1153
1154
  /**
@@ -1182,6 +1183,7 @@ export class SanityClient {
1182
1183
  this.agent = {
1183
1184
  action: new AgentActionsClient(this, this.#httpRequest),
1184
1185
  }
1186
+ this.functions = new FunctionsClient(this, this.#httpRequest)
1185
1187
  this.releases = new ReleasesClient(this, this.#httpRequest)
1186
1188
 
1187
1189
  this.observable = new ObservableSanityClient(httpRequest, config)
@@ -1773,15 +1775,12 @@ export class SanityClient {
1773
1775
  delete(id: string, options: AllDocumentIdsMutationOptions): Promise<MultipleMutationResult>
1774
1776
  /**
1775
1777
  * Deletes a document with the given document ID.
1776
- * Returns a promise that resolves to the deleted document.
1778
+ * Returns a promise that resolves to a mutation result object containing the deleted document ID.
1777
1779
  *
1778
1780
  * @param id - Document ID to delete
1779
1781
  * @param options - Options for the mutation
1780
1782
  */
1781
- delete<R extends Record<string, Any> = Record<string, Any>>(
1782
- id: string,
1783
- options?: BaseMutationOptions,
1784
- ): Promise<SanityDocument<R>>
1783
+ delete(id: string, options?: BaseMutationOptions): Promise<MultipleMutationResult>
1785
1784
  /**
1786
1785
  * Deletes one or more documents matching the given query or document ID.
1787
1786
  * Returns a promise that resolves to first deleted document.
@@ -1828,15 +1827,15 @@ export class SanityClient {
1828
1827
  ): Promise<MultipleMutationResult>
1829
1828
  /**
1830
1829
  * Deletes one or more documents matching the given query or document ID.
1831
- * Returns a promise that resolves to first deleted document.
1830
+ * Returns a promise that resolves to a mutation result object containing the document IDs that were deleted.
1832
1831
  *
1833
1832
  * @param selection - An object with either an `id` or `query` key defining what to delete
1834
1833
  * @param options - Options for the mutation
1835
1834
  */
1836
- delete<R extends Record<string, Any> = Record<string, Any>>(
1835
+ delete(
1837
1836
  selection: MutationSelection,
1838
1837
  options?: BaseMutationOptions,
1839
- ): Promise<SanityDocument<R>>
1838
+ ): Promise<MultipleMutationResult>
1840
1839
  delete<R extends Record<string, Any> = Record<string, Any>>(
1841
1840
  selection: string | MutationSelection,
1842
1841
  options?:
@@ -2070,7 +2069,7 @@ export class SanityClient {
2070
2069
  ): Promise<MultipleMutationResult>
2071
2070
  /**
2072
2071
  * Perform mutation operations against the configured dataset
2073
- * Returns a promise that resolves to the first mutated document.
2072
+ * Returns a promise that resolves to a mutation result object containing the mutated document IDs.
2074
2073
  *
2075
2074
  * @param operations - Mutation operations to execute
2076
2075
  * @param options - Mutation options
@@ -2078,7 +2077,7 @@ export class SanityClient {
2078
2077
  mutate<R extends Record<string, Any> = Record<string, Any>>(
2079
2078
  operations: Mutation<R>[] | Patch | Transaction,
2080
2079
  options?: BaseMutationOptions,
2081
- ): Promise<SanityDocument<R>>
2080
+ ): Promise<MultipleMutationResult>
2082
2081
  mutate<R extends Record<string, Any> = Record<string, Any>>(
2083
2082
  operations: Mutation<R>[] | Patch | Transaction,
2084
2083
  options?:
@@ -8,6 +8,7 @@ import type {
8
8
  Any,
9
9
  HttpRequest,
10
10
  InitializedClientConfig,
11
+ MediaLibraryAssetDocument,
11
12
  SanityAssetDocument,
12
13
  SanityImageAssetDocument,
13
14
  UploadBody,
@@ -67,7 +68,12 @@ export class ObservableAssetsClient {
67
68
  assetType: 'file' | 'image',
68
69
  body: UploadBody,
69
70
  options?: UploadClientConfig,
70
- ): Observable<UploadEvent<{document: SanityAssetDocument | SanityImageAssetDocument}>> {
71
+ ): Observable<
72
+ UploadEvent<
73
+ | {document: SanityAssetDocument | SanityImageAssetDocument}
74
+ | {asset: MediaLibraryAssetDocument}
75
+ >
76
+ > {
71
77
  return _upload(this.#client, this.#httpRequest, assetType, body, options)
72
78
  }
73
79
  }
@@ -84,6 +90,16 @@ export class AssetsClient {
84
90
  /**
85
91
  * Uploads a file asset to the configured dataset
86
92
  *
93
+ * Note: when the client is configured against a Media Library
94
+ * (`resource: {type: 'media-library', id}`), this resolves to a
95
+ * {@link MediaLibraryAssetDocument} at runtime, not to a
96
+ * {@link SanityAssetDocument}. The declared type cannot express that: the
97
+ * shape depends on the client's configuration rather than on the arguments,
98
+ * so an overload cannot discriminate it, and widening the return type into a
99
+ * union would be a breaking change for every existing caller. Narrow the
100
+ * result yourself (for example, check for `currentVersion`) if you upload to
101
+ * a Media Library. Typing this honestly is deferred to the next major.
102
+ *
87
103
  * @param assetType - Asset type (file)
88
104
  * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
89
105
  * @param options - Options to use for the upload
@@ -96,6 +112,10 @@ export class AssetsClient {
96
112
  /**
97
113
  * Uploads an image asset to the configured dataset
98
114
  *
115
+ * Note: against a Media Library this resolves to a
116
+ * {@link MediaLibraryAssetDocument} at runtime. See the `'file'` overload
117
+ * above for why the declared type cannot say so.
118
+ *
99
119
  * @param assetType - Asset type (image)
100
120
  * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream.
101
121
  * @param options - Options to use for the upload
@@ -121,19 +141,48 @@ export class AssetsClient {
121
141
  assetType: 'file' | 'image',
122
142
  body: UploadBody,
123
143
  options?: UploadClientConfig,
124
- ): Promise<SanityAssetDocument | SanityImageAssetDocument> {
125
- type Doc = {document: SanityAssetDocument | SanityImageAssetDocument}
144
+ ): Promise<SanityAssetDocument | SanityImageAssetDocument | MediaLibraryAssetDocument> {
145
+ type Doc =
146
+ | {document: SanityAssetDocument | SanityImageAssetDocument}
147
+ | {asset: MediaLibraryAssetDocument}
126
148
  const observable = _upload<Doc>(this.#client, this.#httpRequest, assetType, body, options)
127
149
  return lastValueFrom(
128
150
  observable.pipe(
129
151
  filter((event): event is UploadResponseEvent<Doc> => event.type === 'response'),
130
- map((event) => event.body.document),
152
+ map((event) => pluckUploadedAsset(event.body)),
131
153
  ),
132
154
  )
133
155
  }
134
156
  }
135
157
 
136
- function _upload<T = {document: SanityAssetDocument | SanityImageAssetDocument}>(
158
+ /**
159
+ * Content Lake's upload endpoint responds with `{document: ...}`; the Media
160
+ * Library upload endpoint responds with `{asset: ...}` instead (a
161
+ * `sanity.asset` document, not a Content Lake asset document). Narrowing on
162
+ * the response body itself - rather than on the client's `resource` config -
163
+ * keeps this correct regardless of how the two are ever wired together.
164
+ */
165
+ function isMediaLibraryUploadBody(
166
+ body:
167
+ | {document: SanityAssetDocument | SanityImageAssetDocument}
168
+ | {asset: MediaLibraryAssetDocument},
169
+ ): body is {asset: MediaLibraryAssetDocument} {
170
+ return 'asset' in body
171
+ }
172
+
173
+ function pluckUploadedAsset(
174
+ body:
175
+ | {document: SanityAssetDocument | SanityImageAssetDocument}
176
+ | {asset: MediaLibraryAssetDocument},
177
+ ): SanityAssetDocument | SanityImageAssetDocument | MediaLibraryAssetDocument {
178
+ return isMediaLibraryUploadBody(body) ? body.asset : body.document
179
+ }
180
+
181
+ function _upload<
182
+ T =
183
+ | {document: SanityAssetDocument | SanityImageAssetDocument}
184
+ | {asset: MediaLibraryAssetDocument},
185
+ >(
137
186
  client: SanityClient | ObservableSanityClient,
138
187
  _httpRequest: HttpRequest,
139
188
  assetType: 'image' | 'file',
@@ -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
  }
@@ -667,23 +667,6 @@ function _mapDataResponse(
667
667
  /**
668
668
  * @internal
669
669
  */
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
670
  export function _dataRequest(
688
671
  client: Client,
689
672
  httpRequest: HttpRequest,
@@ -1139,7 +1122,7 @@ export function _prepareRequest(client: Client, options: RequestObservableOption
1139
1122
  *
1140
1123
  * @internal
1141
1124
  */
1142
- export function _observe<R>(
1125
+ function _observe<R>(
1143
1126
  userSignal: AbortSignal | undefined,
1144
1127
  run: (signal: AbortSignal) => Promise<R>,
1145
1128
  ): Observable<R> {
@@ -1171,7 +1154,7 @@ export function _observe<R>(
1171
1154
  */
1172
1155
  export function _request<R>(client: Client, httpRequest: HttpRequest, options: Any): Promise<R> {
1173
1156
  const reqOptions = _prepareRequest(client, options)
1174
- return httpRequest(reqOptions).then((body) => body as R)
1157
+ return httpRequest(reqOptions, client.config().requestHandler).then((body) => body as R)
1175
1158
  }
1176
1159
 
1177
1160
  /**
@@ -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)