@sanity/sdk 0.0.0-rc.6 → 0.0.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 (53) hide show
  1. package/README.md +7 -15
  2. package/dist/index.d.ts +562 -234
  3. package/dist/index.js +515 -256
  4. package/dist/index.js.map +1 -1
  5. package/package.json +12 -10
  6. package/src/_exports/index.ts +17 -2
  7. package/src/auth/dashboardUtils.test.ts +41 -0
  8. package/src/auth/dashboardUtils.ts +12 -0
  9. package/src/auth/getOrganizationVerificationState.test.ts +197 -0
  10. package/src/auth/getOrganizationVerificationState.ts +73 -0
  11. package/src/auth/handleAuthCallback.test.ts +2 -0
  12. package/src/auth/handleAuthCallback.ts +1 -0
  13. package/src/auth/logout.test.ts +1 -0
  14. package/src/auth/logout.ts +1 -0
  15. package/src/auth/refreshStampedToken.ts +1 -0
  16. package/src/auth/studioModeAuth.test.ts +1 -1
  17. package/src/auth/studioModeAuth.ts +1 -0
  18. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -0
  19. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -0
  20. package/src/client/clientStore.ts +22 -18
  21. package/src/comlink/node/actions/releaseNode.ts +16 -14
  22. package/src/config/__tests__/handles.test.ts +30 -0
  23. package/src/config/handles.ts +67 -0
  24. package/src/config/sanityConfig.ts +44 -16
  25. package/src/document/actions.ts +188 -60
  26. package/src/document/applyDocumentActions.ts +12 -5
  27. package/src/document/documentStore.test.ts +70 -121
  28. package/src/document/documentStore.ts +57 -27
  29. package/src/document/patchOperations.test.ts +1 -1
  30. package/src/document/patchOperations.ts +39 -39
  31. package/src/document/sharedListener.ts +3 -1
  32. package/src/favorites/favorites.test.ts +237 -0
  33. package/src/favorites/favorites.ts +122 -0
  34. package/src/preview/resolvePreview.test.ts +3 -4
  35. package/src/preview/subscribeToStateAndFetchBatches.test.ts +1 -1
  36. package/src/preview/subscribeToStateAndFetchBatches.ts +4 -2
  37. package/src/project/organizationVerification.test.ts +35 -0
  38. package/src/project/organizationVerification.ts +26 -0
  39. package/src/projection/getProjectionState.ts +36 -11
  40. package/src/projection/resolveProjection.test.ts +3 -4
  41. package/src/projection/resolveProjection.ts +35 -9
  42. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  43. package/src/projection/subscribeToStateAndFetchBatches.ts +4 -2
  44. package/src/query/queryStore.test.ts +12 -12
  45. package/src/query/queryStore.ts +71 -42
  46. package/src/releases/getPerspectiveState.test.ts +192 -0
  47. package/src/releases/getPerspectiveState.ts +93 -0
  48. package/src/releases/releasesStore.test.ts +170 -0
  49. package/src/releases/releasesStore.ts +89 -0
  50. package/src/releases/utils/sortReleases.test.ts +336 -0
  51. package/src/releases/utils/sortReleases.ts +48 -0
  52. package/src/utils/listenQuery.test.ts +302 -0
  53. package/src/utils/listenQuery.ts +128 -0
@@ -0,0 +1,336 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {type ReleaseDocument} from '../releasesStore'
4
+ import {sortReleases} from './sortReleases'
5
+
6
+ // Mock function to create a release document
7
+ function createReleaseMock(
8
+ value: Partial<
9
+ Omit<ReleaseDocument, 'metadata'> & {
10
+ metadata: Partial<ReleaseDocument['metadata']>
11
+ }
12
+ >,
13
+ ): ReleaseDocument {
14
+ const id: string = typeof value['_id'] === 'string' ? value['_id'] : 'mockId'
15
+ return {
16
+ _id: id,
17
+ _rev: 'rev',
18
+ _type: 'release',
19
+ _createdAt: new Date().toISOString(),
20
+ _updatedAt: new Date().toISOString(),
21
+ state: 'active',
22
+ ...value,
23
+ metadata: {
24
+ title: `Release ${id}`,
25
+ releaseType: 'asap',
26
+ ...value.metadata,
27
+ },
28
+ name: `Release ${id}`,
29
+ }
30
+ }
31
+
32
+ describe('sortReleases()', () => {
33
+ it('should return the asap releases ordered by createdAt', () => {
34
+ const releases: ReleaseDocument[] = [
35
+ createReleaseMock({
36
+ _id: '_.releases.rasap1',
37
+ _createdAt: '2024-10-24T00:00:00Z',
38
+ metadata: {
39
+ releaseType: 'asap',
40
+ },
41
+ }),
42
+ createReleaseMock({
43
+ _id: '_.releases.rasap2',
44
+ _createdAt: '2024-10-25T00:00:00Z',
45
+ metadata: {
46
+ releaseType: 'asap',
47
+ },
48
+ }),
49
+ ]
50
+ const sorted = sortReleases(releases)
51
+ const expectedOrder = ['rasap2', 'rasap1']
52
+ expectedOrder.forEach((expectedName, idx) => {
53
+ expect(sorted[idx]['_id']).toContain(expectedName)
54
+ })
55
+ })
56
+
57
+ it('should return the scheduled releases ordered by intendedPublishAt or publishAt', () => {
58
+ const releases: ReleaseDocument[] = [
59
+ createReleaseMock({
60
+ _id: '_.releases.rfuture2',
61
+ metadata: {
62
+ releaseType: 'scheduled',
63
+ intendedPublishAt: '2024-11-25T00:00:00Z',
64
+ },
65
+ }),
66
+ createReleaseMock({
67
+ _id: '_.releases.rfuture1',
68
+ metadata: {
69
+ releaseType: 'scheduled',
70
+ intendedPublishAt: '2024-11-23T00:00:00Z',
71
+ },
72
+ }),
73
+ createReleaseMock({
74
+ _id: '_.releases.rfuture4',
75
+ state: 'scheduled',
76
+ publishAt: '2024-11-31T00:00:00Z',
77
+ metadata: {
78
+ releaseType: 'scheduled',
79
+ intendedPublishAt: '2024-10-20T00:00:00Z',
80
+ },
81
+ }),
82
+ createReleaseMock({
83
+ _id: '_.releases.rfuture3',
84
+ state: 'scheduled',
85
+ publishAt: '2024-11-26T00:00:00Z',
86
+ metadata: {
87
+ releaseType: 'scheduled',
88
+ },
89
+ }),
90
+ createReleaseMock({
91
+ _id: '_.releases.rfuture5',
92
+ metadata: {
93
+ releaseType: 'scheduled',
94
+ },
95
+ }),
96
+ createReleaseMock({
97
+ _id: '_.releases.rfuture6',
98
+ metadata: {
99
+ releaseType: 'scheduled',
100
+ },
101
+ }),
102
+ ]
103
+ const sorted = sortReleases(releases)
104
+ const expectedOrder = ['rfuture4', 'rfuture3', 'rfuture2', 'rfuture1', 'rfuture5', 'rfuture6']
105
+ expectedOrder.forEach((expectedName, idx) => {
106
+ expect(sorted[idx]['_id']).toContain(expectedName)
107
+ })
108
+ })
109
+
110
+ it('should return the scheduled releases ordered by intendedPublishAt or publishAt, with missing dates at the end', () => {
111
+ const releases: ReleaseDocument[] = [
112
+ createReleaseMock({
113
+ _id: '_.releases.rfuture2',
114
+ metadata: {
115
+ releaseType: 'scheduled',
116
+ intendedPublishAt: '2024-11-25T00:00:00Z',
117
+ },
118
+ }),
119
+ createReleaseMock({
120
+ _id: '_.releases.rfuture1',
121
+ metadata: {
122
+ releaseType: 'scheduled',
123
+ intendedPublishAt: '2024-11-23T00:00:00Z',
124
+ },
125
+ }),
126
+ createReleaseMock({
127
+ _id: '_.releases.rfuture4',
128
+ state: 'scheduled',
129
+ publishAt: '2024-11-31T00:00:00Z',
130
+ metadata: {
131
+ releaseType: 'scheduled',
132
+ intendedPublishAt: '2024-10-20T00:00:00Z',
133
+ },
134
+ }),
135
+ createReleaseMock({
136
+ _id: '_.releases.rfuture3',
137
+ state: 'scheduled',
138
+ publishAt: '2024-11-26T00:00:00Z',
139
+ metadata: {
140
+ releaseType: 'scheduled',
141
+ },
142
+ }),
143
+ createReleaseMock({
144
+ _id: '_.releases.rfuture5',
145
+ state: 'scheduled',
146
+ publishAt: '2024-11-27T00:00:00Z',
147
+ metadata: {
148
+ releaseType: 'scheduled',
149
+ },
150
+ }),
151
+ createReleaseMock({
152
+ _id: '_.releases.rfuture6',
153
+ metadata: {
154
+ releaseType: 'scheduled',
155
+ },
156
+ }),
157
+ createReleaseMock({
158
+ _id: '_.releases.rfuture7',
159
+ metadata: {
160
+ releaseType: 'scheduled',
161
+ },
162
+ }),
163
+ createReleaseMock({
164
+ _id: '_.releases.rfuture8',
165
+ state: 'scheduled',
166
+ publishAt: '2024-11-28T00:00:00Z',
167
+ metadata: {
168
+ releaseType: 'scheduled',
169
+ },
170
+ }),
171
+ ]
172
+ const sorted = sortReleases(releases)
173
+ const expectedOrder = [
174
+ 'rfuture4',
175
+ 'rfuture8',
176
+ 'rfuture5',
177
+ 'rfuture3',
178
+ 'rfuture2',
179
+ 'rfuture1',
180
+ 'rfuture6',
181
+ 'rfuture7',
182
+ ]
183
+ expectedOrder.forEach((expectedName, idx) => {
184
+ expect(sorted[idx]['_id']).toContain(expectedName)
185
+ })
186
+ })
187
+
188
+ it('should correctly handle scheduled releases with and without dates in both comparison directions', () => {
189
+ const specificCase = [
190
+ createReleaseMock({
191
+ _id: '_.releases.rwithdate',
192
+ metadata: {
193
+ releaseType: 'scheduled',
194
+ intendedPublishAt: '2024-11-29T00:00:00Z',
195
+ },
196
+ }),
197
+ createReleaseMock({
198
+ _id: '_.releases.rnodate',
199
+ metadata: {
200
+ releaseType: 'scheduled',
201
+ },
202
+ }),
203
+ ]
204
+ const sortedSpecific = sortReleases(specificCase)
205
+ expect(sortedSpecific[0]._id).toContain('rwithdate')
206
+ expect(sortedSpecific[1]._id).toContain('rnodate')
207
+
208
+ // Test the reverse order to ensure we hit both comparison branches
209
+ const reversedSpecific = [...specificCase].reverse()
210
+ const sortedReversedSpecific = sortReleases(reversedSpecific)
211
+ expect(sortedReversedSpecific[0]._id).toContain('rwithdate')
212
+ expect(sortedReversedSpecific[1]._id).toContain('rnodate')
213
+ })
214
+
215
+ it('should return the undecided releases ordered by createdAt', () => {
216
+ const releases: ReleaseDocument[] = [
217
+ createReleaseMock({
218
+ _id: '_.releases.rundecided1',
219
+ _createdAt: '2024-10-25T00:00:00Z',
220
+ metadata: {
221
+ releaseType: 'undecided',
222
+ },
223
+ }),
224
+ createReleaseMock({
225
+ _id: '_.releases.rundecided2',
226
+ _createdAt: '2024-10-26T00:00:00Z',
227
+ metadata: {
228
+ releaseType: 'undecided',
229
+ },
230
+ }),
231
+ ]
232
+ const sorted = sortReleases(releases)
233
+ const expectedOrder = ['rundecided2', 'rundecided1']
234
+ expectedOrder.forEach((expectedName, idx) => {
235
+ expect(sorted[idx]['_id']).toContain(expectedName)
236
+ })
237
+ })
238
+
239
+ it('should correctly sort asap vs non-asap releases in both directions', () => {
240
+ const releases: ReleaseDocument[] = [
241
+ createReleaseMock({
242
+ _id: '_.releases.rasap1',
243
+ _createdAt: '2024-10-24T00:00:00Z',
244
+ metadata: {
245
+ releaseType: 'asap',
246
+ },
247
+ }),
248
+ createReleaseMock({
249
+ _id: '_.releases.rscheduled1',
250
+ metadata: {
251
+ releaseType: 'scheduled',
252
+ intendedPublishAt: '2024-11-23T00:00:00Z',
253
+ },
254
+ }),
255
+ createReleaseMock({
256
+ _id: '_.releases.runknown1',
257
+ metadata: {
258
+ releaseType: 'unknown' as unknown as 'asap' | 'scheduled' | 'undecided',
259
+ },
260
+ }),
261
+ createReleaseMock({
262
+ _id: '_.releases.rasap2',
263
+ _createdAt: '2024-10-25T00:00:00Z',
264
+ metadata: {
265
+ releaseType: 'asap',
266
+ },
267
+ }),
268
+ ]
269
+ const sorted = sortReleases(releases)
270
+ const expectedOrder = ['rscheduled1', 'runknown1', 'rasap2', 'rasap1']
271
+ expectedOrder.forEach((expectedName, idx) => {
272
+ expect(sorted[idx]['_id']).toContain(expectedName)
273
+ })
274
+
275
+ // Test with releases in reverse order to ensure both comparison directions are covered
276
+ const reversedReleases = [...releases].reverse()
277
+ const sortedReversed = sortReleases(reversedReleases)
278
+ const expectedReversedOrder = ['runknown1', 'rscheduled1', 'rasap2', 'rasap1']
279
+ expectedReversedOrder.forEach((expectedName, idx) => {
280
+ expect(sortedReversed[idx]['_id']).toContain(expectedName)
281
+ })
282
+ })
283
+
284
+ it("should gracefully combine all release types, and sort them by 'undecided', 'scheduled', 'asap'", () => {
285
+ const releases = [
286
+ createReleaseMock({
287
+ _id: '_.releases.rasap2',
288
+ _createdAt: '2024-10-25T00:00:00Z',
289
+ metadata: {
290
+ releaseType: 'asap',
291
+ },
292
+ }),
293
+ createReleaseMock({
294
+ _id: '_.releases.rasap1',
295
+ _createdAt: '2024-10-24T00:00:00Z',
296
+ metadata: {
297
+ releaseType: 'asap',
298
+ },
299
+ }),
300
+ createReleaseMock({
301
+ _id: '_.releases.rundecided2',
302
+ _createdAt: '2024-10-26T00:00:00Z',
303
+ metadata: {
304
+ releaseType: 'undecided',
305
+ },
306
+ }),
307
+ createReleaseMock({
308
+ _id: '_.releases.rfuture4',
309
+ state: 'scheduled',
310
+ publishAt: '2024-11-31T00:00:00Z',
311
+ metadata: {
312
+ releaseType: 'scheduled',
313
+ intendedPublishAt: '2024-10-20T00:00:00Z',
314
+ },
315
+ }),
316
+ createReleaseMock({
317
+ _id: '_.releases.rfuture1',
318
+ metadata: {
319
+ releaseType: 'scheduled',
320
+ intendedPublishAt: '2024-11-23T00:00:00Z',
321
+ },
322
+ }),
323
+ createReleaseMock({
324
+ _id: '_.releases.runknown1',
325
+ metadata: {
326
+ releaseType: 'unknown' as unknown as 'asap' | 'scheduled' | 'undecided',
327
+ },
328
+ }),
329
+ ]
330
+ const sorted = sortReleases(releases)
331
+ const expectedOrder = ['rundecided2', 'rfuture4', 'rfuture1', 'runknown1', 'rasap2', 'rasap1']
332
+ expectedOrder.forEach((expectedName, idx) => {
333
+ expect(sorted[idx]['_id']).toContain(expectedName)
334
+ })
335
+ })
336
+ })
@@ -0,0 +1,48 @@
1
+ import {type ReleaseDocument} from '../releasesStore'
2
+
3
+ // mirrors the order of the releases in the releases list in Studio
4
+ // https://github.com/sanity-io/sanity/blob/main/packages/sanity/src/core/releases/hooks/utils.ts
5
+ export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[] {
6
+ // The order should always be:
7
+ // [undecided (sortByCreatedAt), scheduled(sortBy publishAt || metadata.intendedPublishAt), asap(sortByCreatedAt)]
8
+ return [...releases].sort((a, b) => {
9
+ // undecided are always first, then by createdAt descending
10
+ if (a.metadata.releaseType === 'undecided' && b.metadata.releaseType !== 'undecided') {
11
+ return -1
12
+ }
13
+ if (a.metadata.releaseType !== 'undecided' && b.metadata.releaseType === 'undecided') {
14
+ return 1
15
+ }
16
+ if (a.metadata.releaseType === 'undecided' && b.metadata.releaseType === 'undecided') {
17
+ // Sort by createdAt
18
+ return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime()
19
+ }
20
+
21
+ // Scheduled are always at the middle, then by publishAt descending
22
+ if (a.metadata.releaseType === 'scheduled' && b.metadata.releaseType === 'scheduled') {
23
+ const aPublishAt = a['publishAt'] || a.metadata['intendedPublishAt']
24
+ if (!aPublishAt) {
25
+ return 1
26
+ }
27
+ const bPublishAt = b['publishAt'] || b.metadata['intendedPublishAt']
28
+ if (!bPublishAt) {
29
+ return -1
30
+ }
31
+ return new Date(bPublishAt).getTime() - new Date(aPublishAt).getTime()
32
+ }
33
+
34
+ // ASAP are always last, then by createdAt descending
35
+ if (a.metadata.releaseType === 'asap' && b.metadata.releaseType !== 'asap') {
36
+ return 1
37
+ }
38
+ if (a.metadata.releaseType !== 'asap' && b.metadata.releaseType === 'asap') {
39
+ return -1
40
+ }
41
+ if (a.metadata.releaseType === 'asap' && b.metadata.releaseType === 'asap') {
42
+ // Sort by createdAt
43
+ return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime()
44
+ }
45
+
46
+ return 0
47
+ })
48
+ }
@@ -0,0 +1,302 @@
1
+ import {
2
+ type ListenEvent,
3
+ type MutationEvent,
4
+ type SanityClient,
5
+ type SanityDocument,
6
+ type WelcomeEvent,
7
+ } from '@sanity/client'
8
+ import {of, Subject, throwError} from 'rxjs'
9
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
10
+
11
+ import {listenQuery} from './listenQuery'
12
+
13
+ describe('listenQuery', () => {
14
+ const mockFetch = vi.fn()
15
+ const mockListen = vi.fn()
16
+
17
+ const mockClient = {
18
+ observable: {
19
+ fetch: mockFetch,
20
+ },
21
+ listen: mockListen,
22
+ } as unknown as SanityClient
23
+
24
+ const mockDoc: SanityDocument = {
25
+ _id: 'doc1',
26
+ _type: 'test',
27
+ _rev: 'rev1',
28
+ _createdAt: new Date().toISOString(),
29
+ _updatedAt: new Date().toISOString(),
30
+ }
31
+ const mockQuery = '*[_type == "test"]'
32
+
33
+ const createMutationEvent = (
34
+ transition: 'update' | 'appear' | 'disappear',
35
+ eventId: string,
36
+ transactionId: string,
37
+ ): MutationEvent<SanityDocument> => ({
38
+ type: 'mutation',
39
+ documentId: 'doc1',
40
+ eventId,
41
+ identity: 'test-user',
42
+ mutations: [{create: mockDoc}],
43
+ timestamp: new Date().toISOString(),
44
+ transition,
45
+ visibility: 'query',
46
+ effects: {
47
+ apply: [],
48
+ revert: [],
49
+ },
50
+ result: mockDoc,
51
+ transactionId,
52
+ transactionTotalEvents: 1,
53
+ transactionCurrentEvent: 1,
54
+ })
55
+
56
+ beforeEach(() => {
57
+ vi.clearAllMocks()
58
+ vi.useFakeTimers()
59
+ })
60
+
61
+ afterEach(() => {
62
+ vi.useRealTimers()
63
+ vi.clearAllMocks()
64
+ })
65
+
66
+ it('performs initial fetch and listens for updates', async () => {
67
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
68
+ mockListen.mockReturnValue(events$)
69
+ mockFetch.mockReturnValue(of([mockDoc]))
70
+
71
+ const results: ListenEvent<Record<string, unknown>>[] = []
72
+ const promise = new Promise<void>((resolve) => {
73
+ listenQuery(mockClient, mockQuery).subscribe({
74
+ next: (result) => {
75
+ results.push(result as ListenEvent<Record<string, unknown>>)
76
+ if (results.length === 2) {
77
+ expect(results).toEqual([[mockDoc], [mockDoc, mockDoc]])
78
+ resolve()
79
+ }
80
+ },
81
+ })
82
+ })
83
+
84
+ // Emit welcome event to trigger initial fetch
85
+ events$.next({
86
+ type: 'welcome',
87
+ listenerName: 'test-listener',
88
+ } as WelcomeEvent)
89
+
90
+ // Emit mutation event to trigger refetch
91
+ mockFetch.mockReturnValue(of([mockDoc, mockDoc]))
92
+ events$.next(createMutationEvent('update', 'evt1', 'tx1'))
93
+ vi.advanceTimersByTime(1000)
94
+
95
+ await promise
96
+ })
97
+
98
+ it('handles separate fetch and listen queries', async () => {
99
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
100
+ mockListen.mockReturnValue(events$)
101
+ mockFetch.mockReturnValue(of([mockDoc]))
102
+
103
+ const query = {
104
+ fetch: '*[_type == "test"] {_id, _type}',
105
+ listen: '*[_type == "test"]',
106
+ }
107
+
108
+ const promise = new Promise<void>((resolve) => {
109
+ listenQuery(mockClient, query).subscribe({
110
+ next: () => {
111
+ expect(mockListen).toHaveBeenCalledWith(
112
+ query.listen,
113
+ {},
114
+ expect.objectContaining({
115
+ events: ['welcome', 'mutation', 'reconnect'],
116
+ includeResult: false,
117
+ }),
118
+ )
119
+ expect(mockFetch).toHaveBeenCalledWith(
120
+ query.fetch,
121
+ {},
122
+ expect.objectContaining({
123
+ filterResponse: true,
124
+ }),
125
+ )
126
+ resolve()
127
+ },
128
+ })
129
+ })
130
+
131
+ events$.next({
132
+ type: 'welcome',
133
+ listenerName: 'test-listener',
134
+ } as WelcomeEvent)
135
+ await promise
136
+ })
137
+
138
+ it('filters mutation events based on transitions option', async () => {
139
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
140
+ mockListen.mockReturnValue(events$)
141
+ mockFetch.mockReturnValueOnce(of([mockDoc])).mockReturnValueOnce(of([mockDoc, mockDoc]))
142
+
143
+ const results: ListenEvent<Record<string, unknown>>[] = []
144
+ const promise = new Promise<void>((resolve) => {
145
+ listenQuery(mockClient, mockQuery, {}, {transitions: ['update']}).subscribe({
146
+ next: (result) => {
147
+ results.push(result as ListenEvent<Record<string, unknown>>)
148
+ if (results.length === 2) {
149
+ expect(results).toEqual([[mockDoc], [mockDoc, mockDoc]])
150
+ resolve()
151
+ }
152
+ },
153
+ })
154
+ })
155
+
156
+ events$.next({
157
+ type: 'welcome',
158
+ listenerName: 'test-listener',
159
+ } as WelcomeEvent)
160
+
161
+ // Should trigger refetch (update transition is allowed)
162
+ events$.next(createMutationEvent('update', 'evt1', 'tx1'))
163
+ vi.advanceTimersByTime(1000)
164
+
165
+ // Should not trigger refetch (appear transition not in allowed list)
166
+ events$.next(createMutationEvent('appear', 'evt2', 'tx2'))
167
+
168
+ await promise
169
+ })
170
+
171
+ it('handles errors in fetch', async () => {
172
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
173
+ mockListen.mockReturnValue(events$)
174
+ mockFetch.mockReturnValue(throwError(() => new Error('Fetch failed')))
175
+
176
+ const promise = new Promise<void>((resolve) => {
177
+ listenQuery(mockClient, mockQuery).subscribe({
178
+ error: (error) => {
179
+ expect(error.message).toBe('Fetch failed')
180
+ resolve()
181
+ },
182
+ })
183
+ })
184
+
185
+ events$.next({
186
+ type: 'welcome',
187
+ listenerName: 'test-listener',
188
+ } as WelcomeEvent)
189
+ await promise
190
+ })
191
+
192
+ it('handles errors in listen stream', async () => {
193
+ const error = new Error('Listen failed')
194
+ mockListen.mockReturnValue(throwError(() => error))
195
+
196
+ const promise = new Promise<void>((resolve) => {
197
+ listenQuery(mockClient, mockQuery).subscribe({
198
+ error: (err) => {
199
+ expect(err).toBe(error)
200
+ resolve()
201
+ },
202
+ })
203
+ })
204
+
205
+ await promise
206
+ })
207
+
208
+ it('throttles subsequent fetches after mutations', async () => {
209
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
210
+ mockListen.mockReturnValue(events$)
211
+ mockFetch.mockReturnValue(of([mockDoc]))
212
+
213
+ const results: ListenEvent<Record<string, unknown>>[] = []
214
+ const promise = new Promise<void>((resolve) => {
215
+ listenQuery(mockClient, mockQuery, {}, {throttleTime: 500}).subscribe({
216
+ next: (result) => {
217
+ results.push(result as ListenEvent<Record<string, unknown>>)
218
+ if (results.length === 2) {
219
+ resolve()
220
+ }
221
+ },
222
+ })
223
+ })
224
+
225
+ events$.next({
226
+ type: 'welcome',
227
+ listenerName: 'test-listener',
228
+ } as WelcomeEvent)
229
+
230
+ // Emit two mutations in quick succession
231
+ events$.next(createMutationEvent('update', 'evt1', 'tx1'))
232
+ // Emit another mutation event quickly, should be debounced
233
+ events$.next(createMutationEvent('update', 'evt2', 'tx2'))
234
+ vi.advanceTimersByTime(500) // Advance timer for debounceTime (using specified throttleTime)
235
+
236
+ await promise
237
+
238
+ expect(mockFetch).toHaveBeenCalledTimes(2)
239
+ })
240
+
241
+ it('handles reconnect events', async () => {
242
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
243
+ mockListen.mockReturnValue(events$)
244
+ mockFetch.mockReturnValue(of([mockDoc]))
245
+
246
+ const results: ListenEvent<Record<string, unknown>>[] = []
247
+ const promise = new Promise<void>((resolve) => {
248
+ listenQuery(mockClient, mockQuery).subscribe({
249
+ next: (result) => {
250
+ results.push(result as ListenEvent<Record<string, unknown>>)
251
+ if (results.length === 2) {
252
+ expect(results).toEqual([[mockDoc], [mockDoc]])
253
+ resolve()
254
+ }
255
+ },
256
+ })
257
+ })
258
+
259
+ events$.next({
260
+ type: 'welcome',
261
+ listenerName: 'test-listener',
262
+ } as WelcomeEvent)
263
+ events$.next({type: 'reconnect'})
264
+ vi.advanceTimersByTime(1000)
265
+
266
+ await promise
267
+ })
268
+
269
+ it('rejects first non-welcome reconnect event', async () => {
270
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
271
+ mockListen.mockReturnValue(events$)
272
+
273
+ const promise = new Promise<void>((resolve) => {
274
+ listenQuery(mockClient, mockQuery).subscribe({
275
+ error: (error) => {
276
+ expect(error.message).toBe('Could not establish EventSource connection')
277
+ resolve()
278
+ },
279
+ })
280
+ })
281
+
282
+ events$.next({type: 'reconnect'})
283
+ await promise
284
+ })
285
+
286
+ it('rejects first non-welcome mutation event', async () => {
287
+ const events$ = new Subject<ListenEvent<SanityDocument>>()
288
+ mockListen.mockReturnValue(events$)
289
+
290
+ const promise = new Promise<void>((resolve) => {
291
+ listenQuery(mockClient, mockQuery).subscribe({
292
+ error: (error) => {
293
+ expect(error.message).toBe('Received unexpected type of first event "mutation"')
294
+ resolve()
295
+ },
296
+ })
297
+ })
298
+
299
+ events$.next(createMutationEvent('update', 'evt1', 'tx1'))
300
+ await promise
301
+ })
302
+ })