@sanity/client 6.17.3 → 6.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/client",
3
- "version": "6.17.3",
3
+ "version": "6.18.0",
4
4
  "description": "Client for retrieving, creating and patching data from Sanity.io",
5
5
  "keywords": [
6
6
  "sanity",
@@ -4,6 +4,7 @@ import {AssetsClient, ObservableAssetsClient} from './assets/AssetsClient'
4
4
  import {defaultConfig, initConfig} from './config'
5
5
  import * as dataMethods from './data/dataMethods'
6
6
  import {_listen} from './data/listen'
7
+ import {LiveClient} from './data/live'
7
8
  import {ObservablePatch, Patch} from './data/patch'
8
9
  import {ObservableTransaction, Transaction} from './data/transaction'
9
10
  import {DatasetsClient, ObservableDatasetsClient} from './datasets/DatasetsClient'
@@ -43,6 +44,7 @@ export type {
43
44
  _listen,
44
45
  AssetsClient,
45
46
  DatasetsClient,
47
+ LiveClient,
46
48
  ObservableAssetsClient,
47
49
  ObservableDatasetsClient,
48
50
  ObservableProjectsClient,
@@ -55,6 +57,7 @@ export type {
55
57
  export class ObservableSanityClient {
56
58
  assets: ObservableAssetsClient
57
59
  datasets: ObservableDatasetsClient
60
+ live: LiveClient
58
61
  projects: ObservableProjectsClient
59
62
  users: ObservableUsersClient
60
63
 
@@ -76,6 +79,7 @@ export class ObservableSanityClient {
76
79
 
77
80
  this.assets = new ObservableAssetsClient(this, this.#httpRequest)
78
81
  this.datasets = new ObservableDatasetsClient(this, this.#httpRequest)
82
+ this.live = new LiveClient(this)
79
83
  this.projects = new ObservableProjectsClient(this, this.#httpRequest)
80
84
  this.users = new ObservableUsersClient(this, this.#httpRequest)
81
85
  }
@@ -695,6 +699,7 @@ export class ObservableSanityClient {
695
699
  export class SanityClient {
696
700
  assets: AssetsClient
697
701
  datasets: DatasetsClient
702
+ live: LiveClient
698
703
  projects: ProjectsClient
699
704
  users: UsersClient
700
705
 
@@ -721,6 +726,7 @@ export class SanityClient {
721
726
 
722
727
  this.assets = new AssetsClient(this, this.#httpRequest)
723
728
  this.datasets = new DatasetsClient(this, this.#httpRequest)
729
+ this.live = new LiveClient(this)
724
730
  this.projects = new ProjectsClient(this, this.#httpRequest)
725
731
  this.users = new UsersClient(this, this.#httpRequest)
726
732
 
@@ -257,7 +257,7 @@ export function _dataRequest(
257
257
  const useGet = !isMutation && strQuery.length < getQuerySizeLimit
258
258
  const stringQuery = useGet ? strQuery : ''
259
259
  const returnFirst = options.returnFirst
260
- const {timeout, token, tag, headers, returnQuery} = options
260
+ const {timeout, token, tag, headers, returnQuery, lastLiveEventId} = options
261
261
 
262
262
  const uri = _getDataUrl(client, endpoint, stringQuery)
263
263
 
@@ -274,6 +274,7 @@ export function _dataRequest(
274
274
  returnQuery,
275
275
  perspective: options.perspective,
276
276
  resultSourceMap: options.resultSourceMap,
277
+ lastLiveEventId: Array.isArray(lastLiveEventId) ? lastLiveEventId[0] : lastLiveEventId,
277
278
  canUseCdn: isQuery,
278
279
  signal: options.signal,
279
280
  fetch: options.fetch,
@@ -375,6 +376,10 @@ export function _requestObservable<R>(
375
376
  }
376
377
  }
377
378
 
379
+ if (options.lastLiveEventId) {
380
+ options.query = {...options.query, lastLiveEventId: options.lastLiveEventId}
381
+ }
382
+
378
383
  if (options.returnQuery === false) {
379
384
  options.query = {returnQuery: 'false', ...options.query}
380
385
  }
@@ -0,0 +1,126 @@
1
+ import {Observable} from 'rxjs'
2
+
3
+ import type {ObservableSanityClient, SanityClient} from '../SanityClient'
4
+ import type {Any, LiveEventMessage, LiveEventRestart} from '../types'
5
+ import {_getDataUrl} from './dataMethods'
6
+
7
+ /**
8
+ * @alpha this API is experimental and may change or even be removed
9
+ */
10
+ export class LiveClient {
11
+ #client: SanityClient | ObservableSanityClient
12
+ constructor(client: SanityClient | ObservableSanityClient) {
13
+ this.#client = client
14
+ }
15
+
16
+ events(): Observable<LiveEventMessage | LiveEventRestart> {
17
+ const path = _getDataUrl(this.#client, 'live/events')
18
+ const url = new URL(this.#client.getUrl(path, false))
19
+
20
+ const listenFor = ['restart', 'message'] as const
21
+
22
+ return new Observable((observer) => {
23
+ let es: InstanceType<typeof EventSource> | undefined
24
+ let reconnectTimer: NodeJS.Timeout
25
+ let stopped = false
26
+ // Unsubscribe differs from stopped in that we will never reopen.
27
+ // Once it is`true`, it will never be `false` again.
28
+ let unsubscribed = false
29
+
30
+ open()
31
+
32
+ // EventSource will emit a regular event if it fails to connect, however the API will emit an `error` MessageEvent if the server goes down
33
+ // So we need to handle both cases
34
+ function onError(evt: MessageEvent | Event) {
35
+ if (stopped) {
36
+ return
37
+ }
38
+
39
+ // If the event has a `data` property, then it`s a MessageEvent emitted by the API and we should forward the error and close the connection
40
+ if ('data' in evt) {
41
+ const event = parseEvent(evt)
42
+ observer.error(new Error(event.message, {cause: event}))
43
+ }
44
+
45
+ // Unless we've explicitly stopped the ES (in which case `stopped` should be true),
46
+ // we should never be in a disconnected state. By default, EventSource will reconnect
47
+ // automatically, in which case it sets readyState to `CONNECTING`, but in some cases
48
+ // (like when a laptop lid is closed), it closes the connection. In these cases we need
49
+ // to explicitly reconnect.
50
+ if (es!.readyState === es!.CLOSED) {
51
+ unsubscribe()
52
+ clearTimeout(reconnectTimer)
53
+ reconnectTimer = setTimeout(open, 100)
54
+ }
55
+ }
56
+
57
+ function onMessage(evt: Any) {
58
+ const event = parseEvent(evt)
59
+ return event instanceof Error ? observer.error(event) : observer.next(event)
60
+ }
61
+
62
+ function unsubscribe() {
63
+ if (!es) return
64
+ es.removeEventListener('error', onError)
65
+ for (const type of listenFor) {
66
+ es.removeEventListener(type, onMessage)
67
+ }
68
+ es.close()
69
+ }
70
+
71
+ async function getEventSource() {
72
+ const EventSourceImplementation: typeof EventSource =
73
+ typeof EventSource === 'undefined'
74
+ ? ((await import('@sanity/eventsource')).default as typeof EventSource)
75
+ : EventSource
76
+
77
+ // If the listener has been unsubscribed from before we managed to load the module,
78
+ // do not set up the EventSource.
79
+ if (unsubscribed) {
80
+ return
81
+ }
82
+
83
+ const evs = new EventSourceImplementation(url.toString())
84
+ evs.addEventListener('error', onError)
85
+ for (const type of listenFor) {
86
+ evs.addEventListener(type, onMessage)
87
+ }
88
+ return evs
89
+ }
90
+
91
+ function open() {
92
+ getEventSource()
93
+ .then((eventSource) => {
94
+ if (eventSource) {
95
+ es = eventSource
96
+ // Handle race condition where the observer is unsubscribed before the EventSource is set up
97
+ if (unsubscribed) {
98
+ unsubscribe()
99
+ }
100
+ }
101
+ })
102
+ .catch((reason) => {
103
+ observer.error(reason)
104
+ stop()
105
+ })
106
+ }
107
+
108
+ function stop() {
109
+ stopped = true
110
+ unsubscribe()
111
+ unsubscribed = true
112
+ }
113
+
114
+ return stop
115
+ })
116
+ }
117
+ }
118
+
119
+ function parseEvent(event: MessageEvent) {
120
+ try {
121
+ const data = (event.data && JSON.parse(event.data)) || {}
122
+ return {type: event.type, id: event.lastEventId, ...data}
123
+ } catch (err) {
124
+ return err
125
+ }
126
+ }
package/src/types.ts CHANGED
@@ -306,6 +306,8 @@ export interface RequestObservableOptions extends Omit<RequestOptions, 'url'> {
306
306
  returnQuery?: boolean
307
307
  resultSourceMap?: boolean | 'withKeyArraySelector'
308
308
  perspective?: ClientPerspective
309
+ /** @alpha this API is experimental and may change or even be removed */
310
+ lastLiveEventId?: string
309
311
  }
310
312
 
311
313
  /** @public */
@@ -479,6 +481,8 @@ export interface QueryParams {
479
481
  token?: never
480
482
  /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */
481
483
  useCdn?: never
484
+ /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */
485
+ lastLiveEventId?: never
482
486
  /* eslint-enable @typescript-eslint/no-explicit-any */
483
487
  }
484
488
 
@@ -743,6 +747,8 @@ export interface ResponseQueryOptions extends RequestOptions {
743
747
  // The `cache` and `next` options are specific to the Next.js App Router integration
744
748
  cache?: 'next' extends keyof RequestInit ? RequestInit['cache'] : never
745
749
  next?: ('next' extends keyof RequestInit ? RequestInit : never)['next']
750
+ /** @alpha this API is experimental and may change or even be removed */
751
+ lastLiveEventId?: string | string[] | null
746
752
  }
747
753
 
748
754
  /** @public */
@@ -785,6 +791,8 @@ export interface RawQueryResponse<R> {
785
791
  ms: number
786
792
  result: R
787
793
  resultSourceMap?: ContentSourceMap
794
+ /** @alpha this API is experimental and may change or even be removed */
795
+ syncTags?: SyncTag[]
788
796
  }
789
797
 
790
798
  /** @public */
@@ -999,6 +1007,19 @@ export interface ContentSourceMap {
999
1007
  paths: ContentSourceMapPaths
1000
1008
  }
1001
1009
 
1010
+ /** @alpha this API is experimental and may change or even be removed */
1011
+ export type SyncTag = `s1:${string}`
1012
+ /** @alpha this API is experimental and may change or even be removed */
1013
+ export interface LiveEventRestart {
1014
+ type: 'restart'
1015
+ }
1016
+ /** @alpha this API is experimental and may change or even be removed */
1017
+ export interface LiveEventMessage {
1018
+ type: 'message'
1019
+ id: string
1020
+ tags: SyncTag[]
1021
+ }
1022
+
1002
1023
  export type {
1003
1024
  ContentSourceMapParsedPath,
1004
1025
  ContentSourceMapParsedPathKeyedSegment,