@sanity/sdk 3.0.0-rc.1 → 3.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 (128) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +71 -3
  3. package/dist/_exports/_internal.d.ts.map +1 -1
  4. package/dist/_exports/_internal.js +332 -24
  5. package/dist/_exports/_internal.js.map +1 -1
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-C0o1NOxW.d.ts +3165 -0
  15. package/dist/authStore-C0o1NOxW.d.ts.map +1 -0
  16. package/dist/authStore-CtE9bLHW.js +754 -0
  17. package/dist/authStore-CtE9bLHW.js.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2335
  29. package/dist/index.js +4048 -3878
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-C7yMHPUI.js +1346 -0
  36. package/dist/queryStore-C7yMHPUI.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-UIEiMtC0.js +127 -0
  41. package/dist/telemetryManager-UIEiMtC0.js.map +1 -0
  42. package/dist/version-D8RP_Q59.js +9 -0
  43. package/dist/version-D8RP_Q59.js.map +1 -0
  44. package/package.json +11 -17
  45. package/src/_exports/_internal.ts +1 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +45 -90
  49. package/src/applications/applications.test-d.ts +5 -0
  50. package/src/applications/applications.ts +5 -2
  51. package/src/auth/authMode.test.ts +0 -25
  52. package/src/auth/authMode.ts +3 -6
  53. package/src/auth/authStore.test.ts +1 -1
  54. package/src/auth/authStore.ts +0 -1
  55. package/src/comments/addonDatasetStore.test.ts +215 -0
  56. package/src/comments/addonDatasetStore.ts +249 -0
  57. package/src/comments/buildCommentThreads.test.ts +83 -0
  58. package/src/comments/buildCommentThreads.ts +54 -0
  59. package/src/comments/commentActions.test.ts +540 -0
  60. package/src/comments/commentActions.ts +555 -0
  61. package/src/comments/commentFieldPath.test.ts +40 -0
  62. package/src/comments/commentFieldPath.ts +44 -0
  63. package/src/comments/commentsConstants.ts +80 -0
  64. package/src/comments/commentsStore.test.ts +419 -0
  65. package/src/comments/commentsStore.ts +483 -0
  66. package/src/comments/normalizeComment.test.ts +116 -0
  67. package/src/comments/normalizeComment.ts +45 -0
  68. package/src/comments/observeComments.test.ts +193 -0
  69. package/src/comments/observeComments.ts +113 -0
  70. package/src/comments/reducers.test.ts +277 -0
  71. package/src/comments/reducers.ts +316 -0
  72. package/src/comments/types.ts +249 -0
  73. package/src/comments/weakenReferences.test.ts +47 -0
  74. package/src/comments/weakenReferences.ts +29 -0
  75. package/src/config/sanityConfig.ts +3 -75
  76. package/src/datasets/datasets.test.ts +44 -9
  77. package/src/datasets/datasets.ts +24 -23
  78. package/src/document/applyDocumentActions.test.ts +8 -12
  79. package/src/document/documentConstants.ts +17 -0
  80. package/src/document/documentStore.test.ts +173 -19
  81. package/src/document/documentStore.ts +36 -5
  82. package/src/document/patchOperations.test.ts +27 -0
  83. package/src/document/patchOperations.ts +10 -1
  84. package/src/document/permissions.test.ts +3 -1
  85. package/src/document/reducers.test.ts +357 -0
  86. package/src/document/reducers.ts +125 -27
  87. package/src/document/util.test.ts +46 -0
  88. package/src/document/util.ts +37 -1
  89. package/src/favorites/favorites.test.ts +64 -24
  90. package/src/favorites/favorites.ts +73 -22
  91. package/src/installations/installations.test-d.ts +5 -0
  92. package/src/installations/installations.ts +4 -0
  93. package/src/presence/bifurTransport.test.ts +155 -3
  94. package/src/presence/bifurTransport.ts +79 -12
  95. package/src/presence/paths.test.ts +70 -0
  96. package/src/presence/paths.ts +41 -0
  97. package/src/presence/presenceStore.test.ts +822 -2
  98. package/src/presence/presenceStore.ts +518 -102
  99. package/src/presence/types.ts +175 -3
  100. package/src/preview/types.ts +0 -25
  101. package/src/projection/projectionStore.test.ts +0 -8
  102. package/src/projection/types.ts +0 -8
  103. package/src/query/queryStore.ts +7 -6
  104. package/src/store/createActionBinder.test.ts +51 -29
  105. package/src/store/createActionBinder.ts +16 -62
  106. package/src/store/createSanityInstance.test.ts +0 -68
  107. package/src/store/createSanityInstance.ts +0 -63
  108. package/src/store/createStateSourceAction.ts +1 -1
  109. package/src/store/fetcherStore.test.ts +23 -1
  110. package/src/store/fetcherStore.ts +3 -3
  111. package/src/users/reducers.test.ts +0 -3
  112. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -1147
  113. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
  114. package/dist/_chunks-es/_internal.js +0 -134
  115. package/dist/_chunks-es/_internal.js.map +0 -1
  116. package/dist/_chunks-es/createGroqSearchFilter.js +0 -1864
  117. package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
  118. package/dist/_chunks-es/telemetryManager.js +0 -93
  119. package/dist/_chunks-es/telemetryManager.js.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/src/applications/userApplications.test.ts +0 -126
  122. package/src/applications/userApplications.ts +0 -261
  123. package/src/preview/getPreviewState.test.ts +0 -137
  124. package/src/preview/getPreviewState.ts +0 -69
  125. package/src/preview/resolvePreview.test.ts +0 -64
  126. package/src/preview/resolvePreview.ts +0 -43
  127. package/src/utils/createFetcherStore.test.ts +0 -171
  128. package/src/utils/createFetcherStore.ts +0 -284
@@ -1,69 +0,0 @@
1
- import {map} from 'rxjs'
2
-
3
- import {type DocumentHandle} from '../config/sanityConfig'
4
- import {getProjectionState} from '../projection/getProjectionState'
5
- import {type SanityInstance} from '../store/createSanityInstance'
6
- import {type StateSource} from '../store/createStateSourceAction'
7
- import {PREVIEW_PROJECTION} from './previewConstants'
8
- import {transformProjectionToPreview} from './previewProjectionUtils'
9
- import {type PreviewQueryResult, type PreviewValue, type ValuePending} from './types'
10
-
11
- /**
12
- * @beta
13
- * @deprecated This type is deprecated and will be removed in a future release.
14
- */
15
- export type GetPreviewStateOptions = DocumentHandle
16
-
17
- /**
18
- * @beta
19
- * @deprecated This function is deprecated and will be removed in a future release.
20
- */
21
- export function getPreviewState<TResult extends object>(
22
- instance: SanityInstance,
23
- options: GetPreviewStateOptions,
24
- ): StateSource<ValuePending<TResult>>
25
- /**
26
- * @beta
27
- * @deprecated This function is deprecated and will be removed in a future release.
28
- */
29
- export function getPreviewState(
30
- instance: SanityInstance,
31
- options: GetPreviewStateOptions,
32
- ): StateSource<ValuePending<PreviewValue>>
33
- /**
34
- * @beta
35
- * @deprecated This function is deprecated and will be removed in a future release.
36
- */
37
- export function getPreviewState(
38
- instance: SanityInstance,
39
- options: GetPreviewStateOptions,
40
- ): StateSource<ValuePending<PreviewValue>> {
41
- // Get the projection state
42
- const projectionState = getProjectionState<PreviewQueryResult>(instance, {
43
- ...options,
44
- projection: PREVIEW_PROJECTION,
45
- })
46
-
47
- // Transform helper to convert projection result to preview format
48
- const transformResult = (
49
- current: ReturnType<typeof projectionState.getCurrent>,
50
- ): ValuePending<PreviewValue> => {
51
- if (!current || current.data === null) {
52
- return {data: null, isPending: current?.isPending ?? false}
53
- }
54
-
55
- const previewValue = transformProjectionToPreview(instance, current.data, options.resource)
56
-
57
- return {
58
- data: previewValue,
59
- isPending: current.isPending,
60
- }
61
- }
62
-
63
- // Wrap the state source to transform projection results to preview format
64
- return {
65
- getCurrent: () => transformResult(projectionState.getCurrent()),
66
- subscribe: (callback) => projectionState.subscribe(callback),
67
- observable: projectionState.observable.pipe(map(transformResult)),
68
- }
69
- }
@@ -1,64 +0,0 @@
1
- import {beforeEach, describe, expect, it, vi} from 'vitest'
2
-
3
- import {resolveProjection} from '../projection/resolveProjection'
4
- import {type ProjectionValuePending} from '../projection/types'
5
- import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
- import {resolvePreview} from './resolvePreview'
7
- import {type PreviewQueryResult} from './types'
8
-
9
- vi.mock('../projection/resolveProjection')
10
-
11
- describe('resolvePreview', () => {
12
- let instance: SanityInstance
13
- beforeEach(() => {
14
- vi.clearAllMocks()
15
- instance = createSanityInstance({projectId: 'p', dataset: 'd'})
16
- })
17
-
18
- afterEach(() => {
19
- instance.dispose()
20
- })
21
-
22
- it('resolves and transforms projection result to preview format', async () => {
23
- const mockProjectionResult: PreviewQueryResult = {
24
- _id: 'doc1',
25
- _type: 'article',
26
- _updatedAt: '2024-01-01',
27
- titleCandidates: {title: 'Resolved Title'},
28
- subtitleCandidates: {description: 'Resolved Description'},
29
- media: null,
30
- }
31
-
32
- vi.mocked(resolveProjection).mockResolvedValue({
33
- data: mockProjectionResult,
34
- isPending: false,
35
- } as ProjectionValuePending<PreviewQueryResult>)
36
-
37
- const result = await resolvePreview(instance, {
38
- documentId: 'doc1',
39
- documentType: 'article',
40
- })
41
-
42
- expect(result.data).toEqual({
43
- title: 'Resolved Title',
44
- subtitle: 'Resolved Description',
45
- media: null,
46
- })
47
- expect(result.isPending).toBe(false)
48
- })
49
-
50
- it('returns null data when projection resolves with null', async () => {
51
- vi.mocked(resolveProjection).mockResolvedValue({
52
- data: null,
53
- isPending: false,
54
- })
55
-
56
- const result = await resolvePreview(instance, {
57
- documentId: 'doc1',
58
- documentType: 'article',
59
- })
60
-
61
- expect(result.data).toBeNull()
62
- expect(result.isPending).toBe(false)
63
- })
64
- })
@@ -1,43 +0,0 @@
1
- import {type DocumentHandle} from '../config/sanityConfig'
2
- import {resolveProjection} from '../projection/resolveProjection'
3
- import {type SanityInstance} from '../store/createSanityInstance'
4
- import {PREVIEW_PROJECTION} from './previewConstants'
5
- import {transformProjectionToPreview} from './previewProjectionUtils'
6
- import {type PreviewQueryResult, type PreviewValue, type ValuePending} from './types'
7
-
8
- /**
9
- * @beta
10
- * @deprecated This type is deprecated and will be removed in a future release.
11
- */
12
- export type ResolvePreviewOptions = DocumentHandle
13
-
14
- /**
15
- * @beta
16
- * @deprecated This function is deprecated and will be removed in a future release.
17
- */
18
- export async function resolvePreview(
19
- instance: SanityInstance,
20
- options: ResolvePreviewOptions,
21
- ): Promise<ValuePending<PreviewValue>> {
22
- // Resolve the projection
23
- const projectionResult = await resolveProjection<PreviewQueryResult>(instance, {
24
- ...options,
25
- projection: PREVIEW_PROJECTION,
26
- })
27
-
28
- if (!projectionResult.data) {
29
- return {data: null, isPending: projectionResult.isPending}
30
- }
31
-
32
- // Transform to preview format
33
- const previewValue = transformProjectionToPreview(
34
- instance,
35
- projectionResult.data,
36
- options.resource,
37
- )
38
-
39
- return {
40
- data: previewValue,
41
- isPending: projectionResult.isPending,
42
- }
43
- }
@@ -1,171 +0,0 @@
1
- import {delay, firstValueFrom, of, throwError} from 'rxjs'
2
- import {filter, skip} from 'rxjs/operators'
3
- import {beforeEach, describe, expect, it, vi} from 'vitest'
4
-
5
- import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
- import {createFetcherStore} from './createFetcherStore'
7
-
8
- describe('createFetcherStore', () => {
9
- let instance: SanityInstance
10
-
11
- beforeEach(() => {
12
- instance = createSanityInstance({projectId: 'p', dataset: 'd'})
13
- vi.useRealTimers()
14
- })
15
-
16
- afterEach(() => {
17
- instance.dispose()
18
- })
19
-
20
- it('should fetch data and update state when subscribed', async () => {
21
- const store = createFetcherStore({
22
- name: 'test',
23
- fetcher: () => (param: number) => of(`data-${param}`).pipe(delay(100)),
24
- getKey: (_instance, param: number) => `key-${param}`,
25
- })
26
-
27
- const stateSource = store.getState(instance, 1)
28
- const dataPromise = firstValueFrom(
29
- stateSource.observable.pipe(filter((data) => data !== undefined)),
30
- )
31
-
32
- vi.useFakeTimers()
33
- await vi.advanceTimersByTimeAsync(100)
34
- const data = await dataPromise
35
- expect(data).toBe('data-1')
36
- vi.useRealTimers()
37
-
38
- expect(stateSource.getCurrent()).toBe('data-1')
39
- })
40
-
41
- it('should only fetch once for multiple subscriptions within throttle interval', async () => {
42
- const fetchSpy = vi.fn().mockImplementation((param: number) => of(`data-${param}`))
43
- const store = createFetcherStore({
44
- name: 'test-throttle',
45
- fetcher: () => fetchSpy,
46
- getKey: (_instance, param: number) => `key-${param}`,
47
- fetchThrottleInternal: 1000,
48
- })
49
-
50
- const stateSource = store.getState(instance, 1)
51
- const sub1 = stateSource.subscribe()
52
- const sub2 = stateSource.subscribe()
53
-
54
- await firstValueFrom(stateSource.observable)
55
- expect(fetchSpy).toHaveBeenCalledTimes(1)
56
-
57
- sub1()
58
- sub2()
59
- })
60
-
61
- it('should propagate errors correctly', async () => {
62
- const store = createFetcherStore({
63
- name: 'test-error',
64
- fetcher: () => () => throwError(() => new Error('test error')),
65
- getKey: () => 'key',
66
- })
67
-
68
- const stateSource = store.getState(instance)
69
- const dataPromise = firstValueFrom(
70
- stateSource.observable.pipe(
71
- // the first value will be `undefined` since the store is not populated
72
- skip(1),
73
- ),
74
- )
75
-
76
- await expect(dataPromise).rejects.toThrow()
77
- expect(() => stateSource.getCurrent()).toThrow()
78
- })
79
-
80
- it('should clear state after expiration delay', async () => {
81
- const fetchSpy = vi
82
- .fn()
83
- .mockImplementation((param: number) => of(`data-${param}`).pipe(delay(100)))
84
- const store = createFetcherStore({
85
- name: 'test-expiration',
86
- fetcher: () => fetchSpy,
87
- getKey: (_instance, param: number) => `key-${param}`,
88
- stateExpirationDelay: 1000,
89
- })
90
-
91
- // First subscription
92
- const stateSource1 = store.getState(instance, 1)
93
- const sub1 = stateSource1.subscribe()
94
- await firstValueFrom(stateSource1.observable.pipe(filter((data) => data !== undefined)))
95
- expect(fetchSpy).toHaveBeenCalledTimes(1)
96
-
97
- sub1()
98
-
99
- // Wait for expiration delay
100
- vi.useFakeTimers()
101
- await vi.advanceTimersByTimeAsync(1000)
102
-
103
- // Second subscription
104
- const stateSource2 = store.getState(instance, 1)
105
- const sub2 = stateSource2.subscribe()
106
- await firstValueFrom(stateSource2.observable.pipe(filter((data) => data !== undefined)))
107
- expect(fetchSpy).toHaveBeenCalledTimes(2)
108
-
109
- sub2()
110
- vi.useRealTimers()
111
- })
112
-
113
- it('should throttle fetches based on fetchThrottleInternal', async () => {
114
- const fetchSpy = vi.fn().mockImplementation((param: number) => of(`data-${param}`))
115
- const store = createFetcherStore({
116
- name: 'test-throttle',
117
- fetcher: () => fetchSpy,
118
- getKey: (_instance, param: number) => `key-${param}`,
119
- fetchThrottleInternal: 1000,
120
- })
121
-
122
- const stateSource = store.getState(instance, 1)
123
-
124
- // First subscription
125
- const sub1 = stateSource.subscribe()
126
- await firstValueFrom(stateSource.observable)
127
- expect(fetchSpy).toHaveBeenCalledTimes(1)
128
-
129
- // Second subscription within throttle interval
130
- const stateSource2 = store.getState(instance, 1)
131
- const sub2 = stateSource2.subscribe()
132
- await firstValueFrom(stateSource2.observable)
133
- expect(fetchSpy).toHaveBeenCalledTimes(1)
134
-
135
- // Advance past throttle interval
136
- vi.useFakeTimers()
137
- await vi.advanceTimersByTimeAsync(1000)
138
-
139
- // Third subscription after throttle interval
140
- const stateSource3 = store.getState(instance, 1)
141
- const sub3 = stateSource3.subscribe()
142
- await firstValueFrom(stateSource3.observable)
143
- expect(fetchSpy).toHaveBeenCalledTimes(2)
144
-
145
- sub1()
146
- sub2()
147
- sub3()
148
- vi.useRealTimers()
149
- })
150
-
151
- it('should handle different parameters with different keys', async () => {
152
- const store = createFetcherStore({
153
- name: 'test-params',
154
- fetcher: () => (param: number) => of(`data-${param}`),
155
- getKey: (_instance, param: number) => `key-${param}`,
156
- })
157
-
158
- const stateSource1 = store.getState(instance, 1)
159
- const stateSource2 = store.getState(instance, 2)
160
-
161
- const data1 = await firstValueFrom(
162
- stateSource1.observable.pipe(filter((data) => data !== undefined)),
163
- )
164
- const data2 = await firstValueFrom(
165
- stateSource2.observable.pipe(filter((data) => data !== undefined)),
166
- )
167
-
168
- expect(data1).toBe('data-1')
169
- expect(data2).toBe('data-2')
170
- })
171
- })
@@ -1,284 +0,0 @@
1
- import {asapScheduler, EMPTY, firstValueFrom, from, Observable} from 'rxjs'
2
- import {
3
- catchError,
4
- delay,
5
- filter,
6
- first,
7
- groupBy,
8
- map,
9
- mergeMap,
10
- pairwise,
11
- startWith,
12
- switchMap,
13
- tap,
14
- } from 'rxjs/operators'
15
-
16
- import {bindActionGlobally, type BoundStoreAction} from '../store/createActionBinder'
17
- import {type SanityInstance} from '../store/createSanityInstance'
18
- import {
19
- createStateSourceAction,
20
- type SelectorContext,
21
- type StateSource,
22
- } from '../store/createStateSourceAction'
23
- import {defineStore, type StoreContext} from '../store/defineStore'
24
- import {insecureRandomId} from '../utils/ids'
25
- import {omitProperty} from '../utils/object'
26
- import {setCleanupTimeout} from './setCleanupTimeout'
27
-
28
- interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
29
- /**
30
- * A unique name for this store (for devtools)
31
- */
32
- name: string
33
- /**
34
- * A function that creates the observable that will be used to fetch the data.
35
- */
36
- fetcher: (instance: SanityInstance) => (...params: TParams) => Observable<TData>
37
- /**
38
- * The function used to convert the params into keys that state related to
39
- * those params will be stored.
40
- */
41
- getKey: (instance: SanityInstance, ...params: TParams) => string
42
- /**
43
- * Delay in ms before clearing state after the last subscription is removed.
44
- * This results in react components suspending again due to no previous state
45
- * to show.
46
- */
47
- stateExpirationDelay?: number
48
- /**
49
- * The minimum time (ms) that must elapse since the last fetch for the same key
50
- */
51
- fetchThrottleInternal?: number
52
- }
53
-
54
- interface StoreEntry<TParams extends unknown[], TData> {
55
- params: TParams
56
- instance: SanityInstance
57
- key: string
58
- data?: TData
59
- error?: unknown
60
- subscriptions: string[]
61
- lastFetchInitiatedAt?: string
62
- }
63
-
64
- /**
65
- * Internal helper type
66
- * @public
67
- */
68
- export interface FetcherStoreState<TParams extends unknown[], TData> {
69
- stateByParams: {[TSerializedKey in string]?: StoreEntry<TParams, TData>}
70
- error?: unknown
71
- }
72
-
73
- /**
74
- * Internal helper type
75
- * @public
76
- */
77
- export interface FetcherStore<TParams extends unknown[], TData> {
78
- getState: BoundStoreAction<
79
- FetcherStoreState<TParams, TData>,
80
- TParams,
81
- StateSource<TData | undefined>
82
- >
83
- resolveState: BoundStoreAction<FetcherStoreState<TParams, TData>, TParams, Promise<TData>>
84
- }
85
-
86
- /**
87
- * Creates a store from a function that returns an observable that fetches data
88
- * that supports parameterized state caching.
89
- *
90
- * This function creates a resource store keyed by parameter values (using the
91
- * provided `getKey` function) and returns a state source (via `getState`)
92
- * that components can subscribe to. When a new subscription is added, and if
93
- * enough time has passed since the last fetch (controlled by
94
- * `fetchThrottleInternal`), it invokes the observable factory (via
95
- * `getObservable`) to fetch fresh data. The data is stored in state and can be
96
- * accessed reactively.
97
- *
98
- * Additionally, the store provides a `resolveState` function that returns a
99
- * Promise resolving with the next non-undefined value from the state source.
100
- *
101
- * State expiration is implemented: after the last subscription for a key is
102
- * removed, its state is cleared after `stateExpirationDelay` ms, causing
103
- * components to suspend until fresh data is fetched.
104
- */
105
- export function createFetcherStore<TParams extends unknown[], TData>({
106
- name,
107
- fetcher: getObservable,
108
- getKey,
109
- fetchThrottleInternal = 1000,
110
- stateExpirationDelay = 5000,
111
- }: CreateFetcherStoreOptions<TParams, TData>): FetcherStore<TParams, TData> {
112
- const store = defineStore<FetcherStoreState<TParams, TData>>({
113
- name,
114
- getInitialState: () => ({
115
- stateByParams: {},
116
- }),
117
- initialize: (context) => {
118
- const subscription = subscribeToSubscriptionsAndFetch(context)
119
- return () => subscription.unsubscribe()
120
- },
121
- })
122
-
123
- /**
124
- * For each key in state.stateByParams we set up a grouped stream.
125
- * When a subscription is added (i.e. the subscriptions array length increases)
126
- * and if enough time has elapsed since the last fetch, we update the timestamp
127
- * and call the factory function for that key.
128
- */
129
- const subscribeToSubscriptionsAndFetch = ({
130
- state,
131
- }: StoreContext<FetcherStoreState<TParams, TData>>) => {
132
- return state.observable
133
- .pipe(
134
- // Map the state to an array of [serialized, entry] pairs.
135
- switchMap((s: FetcherStoreState<TParams, TData>) => {
136
- const entries = Object.entries(s.stateByParams)
137
- return entries.length > 0 ? from(entries) : EMPTY
138
- }),
139
- // Group by the serialized key.
140
- groupBy(([key]) => key),
141
- mergeMap((group$) =>
142
- group$.pipe(
143
- // Emit an initial value for pairwise comparisons.
144
- startWith<[string, StoreEntry<TParams, TData> | undefined]>([group$.key, undefined]),
145
- pairwise(),
146
- // Trigger only when the subscriptions array grows.
147
- filter(([[, prevEntry], [, currEntry]]) => {
148
- const prevSubs = prevEntry?.subscriptions ?? []
149
- const currSubs = currEntry?.subscriptions ?? []
150
- return currSubs.length > prevSubs.length
151
- }),
152
- map(([, [, currEntry]]) => currEntry),
153
-
154
- // Only trigger if we haven't fetched recently.
155
- filter((entry) => {
156
- const lastFetch = entry?.lastFetchInitiatedAt
157
- if (!lastFetch) return true
158
- return Date.now() - new Date(lastFetch).getTime() >= fetchThrottleInternal
159
- }),
160
- switchMap((entry) => {
161
- // Retrieve params from the entry
162
- if (!entry) return EMPTY
163
-
164
- // Record that a fetch is being initiated.
165
- state.set('setLastFetchInitiatedAt', (prev: FetcherStoreState<TParams, TData>) => ({
166
- stateByParams: {
167
- ...prev.stateByParams,
168
- [entry.key]: {
169
- ...entry,
170
- ...prev.stateByParams[entry.key],
171
- lastFetchInitiatedAt: new Date().toISOString(),
172
- },
173
- },
174
- }))
175
-
176
- const factoryFn = getObservable(entry.instance)
177
- return factoryFn(...entry.params).pipe(
178
- // the `createStateSourceAction` util requires the update
179
- // to
180
- delay(0, asapScheduler),
181
- tap((data: TData) =>
182
- state.set('setData', (prev: FetcherStoreState<TParams, TData>) => ({
183
- stateByParams: {
184
- ...prev.stateByParams,
185
- [entry.key]: {
186
- ...omitProperty(entry, 'error'),
187
- ...(prev.stateByParams[entry.key]
188
- ? omitProperty(prev.stateByParams[entry.key], 'error')
189
- : {}),
190
- data,
191
- },
192
- },
193
- })),
194
- ),
195
- catchError((error) => {
196
- state.set('setError', (prev) => ({
197
- stateByParams: {
198
- ...prev.stateByParams,
199
- [entry.key]: {
200
- ...entry,
201
- ...prev.stateByParams[entry.key],
202
- error,
203
- },
204
- },
205
- }))
206
-
207
- return EMPTY
208
- }),
209
- )
210
- }),
211
- ),
212
- ),
213
- )
214
- .subscribe({
215
- error: (error) => state.set('setError', {error}),
216
- })
217
- }
218
-
219
- const getState = bindActionGlobally(
220
- store,
221
- createStateSourceAction({
222
- selector: (
223
- {
224
- instance,
225
- state: {stateByParams, error},
226
- }: SelectorContext<FetcherStoreState<TParams, TData>>,
227
- ...params: TParams
228
- ) => {
229
- if (error) throw error
230
- const key = getKey(instance, ...params)
231
- const entry = stateByParams[key]
232
- if (entry?.error) throw entry.error
233
- return entry?.data
234
- },
235
- onSubscribe: ({instance, state}, ...params: TParams) => {
236
- const subscriptionId = insecureRandomId()
237
- const key = getKey(instance, ...params)
238
-
239
- state.set('addSubscription', (prev: FetcherStoreState<TParams, TData>) => ({
240
- stateByParams: {
241
- ...prev.stateByParams,
242
- [key]: {
243
- ...prev.stateByParams[key],
244
- instance,
245
- key,
246
- params: prev.stateByParams[key]?.params || params,
247
- subscriptions: [...(prev.stateByParams[key]?.subscriptions || []), subscriptionId],
248
- },
249
- },
250
- }))
251
-
252
- return () => {
253
- setCleanupTimeout(() => {
254
- state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
255
- const entry = prev.stateByParams[key]
256
- if (!entry) return prev
257
-
258
- const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
259
- if (newSubs.length === 0) {
260
- return {stateByParams: omitProperty(prev.stateByParams, key)}
261
- }
262
-
263
- return {
264
- stateByParams: {
265
- ...prev.stateByParams,
266
- [key]: {
267
- ...entry,
268
- subscriptions: newSubs,
269
- },
270
- },
271
- }
272
- })
273
- }, stateExpirationDelay)
274
- }
275
- },
276
- }),
277
- )
278
-
279
- const resolveState = bindActionGlobally(store, ({instance}, ...params: TParams) =>
280
- firstValueFrom(getState(instance, ...params).observable.pipe(first((i) => i !== undefined))),
281
- )
282
-
283
- return {getState, resolveState}
284
- }