@sanity/sdk 0.0.0-chore-react-18-compat.1 → 0.0.0-chore-react-18-compat.3

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 (134) hide show
  1. package/dist/index.d.ts +441 -322
  2. package/dist/index.js +1685 -1481
  3. package/dist/index.js.map +1 -1
  4. package/package.json +13 -15
  5. package/src/_exports/index.ts +32 -30
  6. package/src/auth/authStore.test.ts +149 -104
  7. package/src/auth/authStore.ts +51 -100
  8. package/src/auth/handleAuthCallback.test.ts +67 -34
  9. package/src/auth/handleAuthCallback.ts +8 -7
  10. package/src/auth/logout.test.ts +61 -29
  11. package/src/auth/logout.ts +26 -28
  12. package/src/auth/refreshStampedToken.test.ts +197 -91
  13. package/src/auth/refreshStampedToken.ts +170 -59
  14. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
  15. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
  16. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
  17. package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
  18. package/src/client/clientStore.test.ts +131 -67
  19. package/src/client/clientStore.ts +117 -116
  20. package/src/comlink/controller/actions/destroyController.test.ts +38 -13
  21. package/src/comlink/controller/actions/destroyController.ts +11 -15
  22. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
  23. package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
  24. package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
  25. package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
  26. package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
  27. package/src/comlink/controller/actions/releaseChannel.ts +22 -21
  28. package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
  29. package/src/comlink/controller/comlinkControllerStore.ts +44 -5
  30. package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
  31. package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
  32. package/src/comlink/node/actions/releaseNode.test.ts +75 -55
  33. package/src/comlink/node/actions/releaseNode.ts +19 -21
  34. package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
  35. package/src/comlink/node/comlinkNodeStore.ts +22 -5
  36. package/src/config/authConfig.ts +79 -0
  37. package/src/config/sanityConfig.ts +48 -0
  38. package/src/datasets/datasets.test.ts +2 -2
  39. package/src/datasets/datasets.ts +18 -5
  40. package/src/document/actions.test.ts +22 -10
  41. package/src/document/actions.ts +44 -56
  42. package/src/document/applyDocumentActions.test.ts +96 -36
  43. package/src/document/applyDocumentActions.ts +140 -99
  44. package/src/document/documentStore.test.ts +103 -155
  45. package/src/document/documentStore.ts +247 -238
  46. package/src/document/listen.ts +56 -55
  47. package/src/document/patchOperations.ts +0 -43
  48. package/src/document/permissions.test.ts +25 -12
  49. package/src/document/permissions.ts +11 -4
  50. package/src/document/processActions.test.ts +41 -8
  51. package/src/document/reducers.test.ts +87 -16
  52. package/src/document/reducers.ts +2 -2
  53. package/src/document/sharedListener.test.ts +34 -16
  54. package/src/document/sharedListener.ts +33 -11
  55. package/src/preview/getPreviewState.test.ts +40 -39
  56. package/src/preview/getPreviewState.ts +68 -56
  57. package/src/preview/previewConstants.ts +43 -0
  58. package/src/preview/previewQuery.test.ts +1 -1
  59. package/src/preview/previewQuery.ts +4 -5
  60. package/src/preview/previewStore.test.ts +13 -58
  61. package/src/preview/previewStore.ts +7 -21
  62. package/src/preview/resolvePreview.test.ts +33 -104
  63. package/src/preview/resolvePreview.ts +11 -21
  64. package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
  65. package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
  66. package/src/preview/util.ts +1 -0
  67. package/src/project/project.test.ts +3 -3
  68. package/src/project/project.ts +28 -5
  69. package/src/projection/getProjectionState.test.ts +188 -72
  70. package/src/projection/getProjectionState.ts +92 -62
  71. package/src/projection/projectionQuery.test.ts +114 -12
  72. package/src/projection/projectionQuery.ts +75 -32
  73. package/src/projection/projectionStore.test.ts +13 -51
  74. package/src/projection/projectionStore.ts +6 -43
  75. package/src/projection/resolveProjection.test.ts +32 -127
  76. package/src/projection/resolveProjection.ts +16 -28
  77. package/src/projection/subscribeToStateAndFetchBatches.test.ts +203 -116
  78. package/src/projection/subscribeToStateAndFetchBatches.ts +140 -85
  79. package/src/projection/types.ts +50 -0
  80. package/src/projection/util.ts +3 -1
  81. package/src/projects/projects.test.ts +13 -4
  82. package/src/projects/projects.ts +6 -1
  83. package/src/query/queryStore.test.ts +10 -47
  84. package/src/query/queryStore.ts +151 -133
  85. package/src/query/queryStoreConstants.ts +2 -0
  86. package/src/store/createActionBinder.test.ts +153 -0
  87. package/src/store/createActionBinder.ts +176 -0
  88. package/src/store/createSanityInstance.test.ts +84 -0
  89. package/src/store/createSanityInstance.ts +124 -0
  90. package/src/store/createStateSourceAction.test.ts +196 -0
  91. package/src/store/createStateSourceAction.ts +260 -0
  92. package/src/store/createStoreInstance.test.ts +81 -0
  93. package/src/store/createStoreInstance.ts +80 -0
  94. package/src/store/createStoreState.test.ts +85 -0
  95. package/src/store/createStoreState.ts +92 -0
  96. package/src/store/defineStore.test.ts +18 -0
  97. package/src/store/defineStore.ts +81 -0
  98. package/src/users/reducers.test.ts +318 -0
  99. package/src/users/reducers.ts +88 -0
  100. package/src/users/types.ts +46 -4
  101. package/src/users/usersConstants.ts +4 -0
  102. package/src/users/usersStore.test.ts +350 -223
  103. package/src/users/usersStore.ts +285 -149
  104. package/src/utils/createFetcherStore.test.ts +6 -7
  105. package/src/utils/createFetcherStore.ts +150 -153
  106. package/src/utils/createGroqSearchFilter.test.ts +75 -0
  107. package/src/utils/createGroqSearchFilter.ts +85 -0
  108. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  109. package/dist/index.cjs +0 -4888
  110. package/dist/index.cjs.map +0 -1
  111. package/dist/index.d.cts +0 -2121
  112. package/src/auth/fetchLoginUrls.test.ts +0 -163
  113. package/src/auth/fetchLoginUrls.ts +0 -74
  114. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  115. package/src/common/createLiveEventSubscriber.ts +0 -55
  116. package/src/common/types.ts +0 -4
  117. package/src/instance/identity.test.ts +0 -46
  118. package/src/instance/identity.ts +0 -29
  119. package/src/instance/sanityInstance.test.ts +0 -77
  120. package/src/instance/sanityInstance.ts +0 -57
  121. package/src/instance/types.ts +0 -37
  122. package/src/preview/getPreviewProjection.ts +0 -45
  123. package/src/resources/README.md +0 -370
  124. package/src/resources/createAction.test.ts +0 -101
  125. package/src/resources/createAction.ts +0 -44
  126. package/src/resources/createResource.test.ts +0 -112
  127. package/src/resources/createResource.ts +0 -102
  128. package/src/resources/createStateSourceAction.test.ts +0 -114
  129. package/src/resources/createStateSourceAction.ts +0 -83
  130. package/src/resources/createStore.test.ts +0 -67
  131. package/src/resources/createStore.ts +0 -46
  132. package/src/store/createStore.test.ts +0 -108
  133. package/src/store/createStore.ts +0 -106
  134. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -1,144 +1,49 @@
1
- import {describe, it, type Mock} from 'vitest'
2
-
3
- import {createSanityInstance} from '../instance/sanityInstance'
4
- import {
5
- createResourceState,
6
- getOrCreateResource,
7
- type InitializedResource,
8
- type ResourceState,
9
- } from '../resources/createResource'
10
- import {insecureRandomId} from '../utils/ids'
11
- import {
12
- projectionStore,
13
- type ProjectionStoreState,
14
- type ProjectionValuePending,
15
- } from './projectionStore'
1
+ import {type SanityDocumentLike} from '@sanity/types'
2
+ import {of} from 'rxjs'
3
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {type DocumentHandle} from '../config/sanityConfig'
6
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
+ import {type StateSource} from '../store/createStateSourceAction'
8
+ import {getProjectionState} from './getProjectionState'
16
9
  import {resolveProjection} from './resolveProjection'
10
+ import {type ProjectionValuePending, type ValidProjection} from './types'
17
11
 
18
- vi.mock('../utils/ids', async (importOriginal) => {
19
- const util = await importOriginal<typeof import('../utils/ids')>()
20
- return {...util, insecureRandomId: vi.fn(util.insecureRandomId)}
21
- })
22
-
23
- vi.mock('../resources/createResource', async (importOriginal) => {
24
- const original = await importOriginal<typeof import('../resources/createResource')>()
25
- return {...original, getOrCreateResource: vi.fn()}
26
- })
12
+ vi.mock('./getProjectionState')
27
13
 
28
14
  describe('resolveProjection', () => {
29
- const instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
30
- const document = {_id: 'exampleId', _type: 'exampleType'}
31
- const projectionString = '{title, description}'
32
- const initialState: ProjectionStoreState = {
33
- documentProjections: {},
34
- lastLiveEventId: null,
35
- subscriptions: {},
36
- syncTags: {},
37
- values: {},
38
- }
39
- let state: ResourceState<ProjectionStoreState>
15
+ let instance: SanityInstance
40
16
 
41
17
  beforeEach(() => {
42
- state = createResourceState(initialState)
18
+ vi.resetAllMocks()
19
+ // Create a mock that returns the correct ProjectionValuePending type
20
+ vi.mocked(getProjectionState).mockReturnValue({
21
+ observable: of({
22
+ data: {title: 'test'},
23
+ isPending: false,
24
+ } as ProjectionValuePending<Record<string, unknown>>),
25
+ } as StateSource<ProjectionValuePending<Record<string, unknown>>>)
26
+
27
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
43
28
  })
44
29
 
45
- it('subscribes and resolves when the projection value is non-null', async () => {
46
- expect(state.get().subscriptions).toEqual({})
47
- ;(insecureRandomId as Mock).mockImplementationOnce(() => 'pseudoRandomId')
48
-
49
- const projectionPromise = resolveProjection(
50
- {state, instance},
51
- {document, projection: projectionString},
52
- )
53
- expect(state.get().subscriptions).toEqual({exampleId: {pseudoRandomId: true}})
54
- expect(state.get().documentProjections).toEqual({exampleId: projectionString})
55
-
56
- state.set('updateDifferentDocument', (prev) => ({
57
- values: {
58
- ...prev.values,
59
- differentId: {data: {title: 'Different Document'}, isPending: false},
60
- },
61
- }))
62
-
63
- expect(state.get().subscriptions).toEqual({exampleId: {pseudoRandomId: true}})
64
-
65
- state.set('updateCorrectDocumentButNull', (prev) => ({
66
- values: {...prev.values, exampleId: {data: null, isPending: true}},
67
- }))
68
-
69
- expect(state.get().subscriptions).toEqual({exampleId: {pseudoRandomId: true}})
70
-
71
- state.set('updateCorrectDocument', (prev) => ({
72
- values: {
73
- ...prev.values,
74
- exampleId: {
75
- data: {title: 'Correct Document', description: 'Test'},
76
- isPending: false,
77
- },
78
- },
79
- }))
80
-
81
- const projectionResult = await projectionPromise
82
- expect(projectionResult).toEqual({
83
- data: {title: 'Correct Document', description: 'Test'},
84
- isPending: false,
85
- })
86
-
87
- // subscription is removed after
88
- expect(state.get().subscriptions).toEqual({})
30
+ afterEach(() => {
31
+ instance.dispose()
89
32
  })
90
33
 
91
- it('resolves with the next emitted state (not current state)', async () => {
92
- const currentValue: ProjectionValuePending<Record<string, unknown>> = {
93
- data: {title: 'Current Document', description: 'Test'},
94
- isPending: false,
34
+ it('resolves a projection and returns the first emitted value with results', async () => {
35
+ const docHandle: DocumentHandle<SanityDocumentLike> = {
36
+ documentId: 'doc123',
37
+ documentType: 'movie',
95
38
  }
96
- state.set('setInitialDocument', (prev) => ({
97
- values: {...prev.values, exampleId: currentValue},
98
- }))
99
- vi.mocked(insecureRandomId).mockImplementationOnce(() => 'pseudoRandomId')
100
- expect(state.get().subscriptions).toEqual({})
39
+ const projection = '{title}' as ValidProjection
101
40
 
102
- const projectionPromise = resolveProjection(
103
- {state, instance},
104
- {document, projection: projectionString},
105
- )
106
- expect(state.get().subscriptions).toEqual({exampleId: {pseudoRandomId: true}})
41
+ const result = await resolveProjection(instance, {...docHandle, projection})
107
42
 
108
- state.set('updateDifferentDocument', (prev) => ({
109
- values: {
110
- ...prev.values,
111
- differentId: {data: {title: 'Different Document'}, isPending: false},
112
- },
113
- }))
114
- expect(state.get().subscriptions).toEqual({exampleId: {pseudoRandomId: true}})
115
-
116
- state.set('updateWithCurrentValue', (prev) => ({
117
- values: {...prev.values, exampleId: currentValue},
118
- }))
119
- expect(state.get().subscriptions).toEqual({exampleId: {pseudoRandomId: true}})
120
-
121
- state.set('updateWithNewValue', (prev) => ({
122
- values: {
123
- ...prev.values,
124
- exampleId: {
125
- data: {title: 'New Value', description: 'Updated'},
126
- isPending: false,
127
- },
128
- },
129
- }))
130
- expect(state.get().subscriptions).toEqual({})
131
-
132
- const projectionResult = await projectionPromise
133
- expect(projectionResult).toEqual({
134
- data: {title: 'New Value', description: 'Updated'},
43
+ expect(getProjectionState).toHaveBeenCalledWith(instance, {...docHandle, projection})
44
+ expect(result).toEqual({
45
+ data: {title: 'test'},
135
46
  isPending: false,
136
47
  })
137
48
  })
138
-
139
- it('calls getOrCreateResource if no state is provided', () => {
140
- vi.mocked(getOrCreateResource).mockReturnValue({state} as InitializedResource<unknown>)
141
- resolveProjection(instance, {document, projection: projectionString})
142
- expect(getOrCreateResource).toHaveBeenCalledWith(instance, projectionStore)
143
- })
144
49
  })
@@ -1,36 +1,24 @@
1
- import {type DocumentHandle} from '../document/patchOperations'
2
- import {type ActionContext, createAction} from '../resources/createAction'
1
+ import {filter, firstValueFrom} from 'rxjs'
2
+
3
+ import {type DocumentHandle} from '../config/sanityConfig'
4
+ import {bindActionByDataset} from '../store/createActionBinder'
3
5
  import {getProjectionState} from './getProjectionState'
4
- import {
5
- projectionStore,
6
- type ProjectionStoreState,
7
- type ProjectionValuePending,
8
- type ValidProjection,
9
- } from './projectionStore'
6
+ import {projectionStore} from './projectionStore'
7
+ import {type ValidProjection} from './types'
10
8
 
11
- interface ResolveProjectionOptions {
12
- document: DocumentHandle
9
+ interface ResolveProjectionOptions extends DocumentHandle {
13
10
  projection: ValidProjection
14
11
  }
15
12
 
16
13
  /**
17
14
  * @beta
18
15
  */
19
- export const resolveProjection = createAction(projectionStore, () => {
20
- return function <TResult extends Record<string, unknown> = Record<string, unknown>>(
21
- this: ActionContext<ProjectionStoreState>,
22
- {document, projection}: ResolveProjectionOptions,
23
- ): Promise<ProjectionValuePending<TResult>> {
24
- const {getCurrent, subscribe} = getProjectionState<TResult>(this, {document, projection})
25
-
26
- return new Promise<ProjectionValuePending<TResult>>((resolve) => {
27
- const unsubscribe = subscribe(() => {
28
- const current = getCurrent()
29
- if (current?.data) {
30
- resolve(current)
31
- unsubscribe()
32
- }
33
- })
34
- })
35
- }
36
- })
16
+ export const resolveProjection = bindActionByDataset(
17
+ projectionStore,
18
+ ({instance}, {projection, ...docHandle}: ResolveProjectionOptions) =>
19
+ firstValueFrom(
20
+ getProjectionState(instance, {...docHandle, projection}).observable.pipe(
21
+ filter((i) => !!i.data),
22
+ ),
23
+ ),
24
+ )
@@ -1,51 +1,40 @@
1
- import {SanityClient, type SyncTag} from '@sanity/client'
2
- import {Observable, of, Subject} from 'rxjs'
3
- import {describe, expect, it, type Mock, vi} from 'vitest'
4
-
5
- import {getClientState} from '../client/clientStore'
6
- import {hashString} from '../common/util'
7
- import {createSanityInstance} from '../instance/sanityInstance'
8
- import {createResourceState, type ResourceState} from '../resources/createResource'
9
- import {type StateSource} from '../resources/createStateSourceAction'
10
- import {type ProjectionQueryResult, type ProjectionStoreState} from './projectionStore'
1
+ import {NEVER, Observable, type Observer} from 'rxjs'
2
+ import {describe, expect, it, vi} from 'vitest'
3
+
4
+ import {getQueryState, resolveQuery} from '../query/queryStore'
5
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
6
+ import {type StateSource} from '../store/createStateSourceAction'
7
+ import {createStoreState, type StoreState} from '../store/createStoreState'
8
+ import {hashString} from '../utils/hashString'
9
+ import {type ProjectionQueryResult} from './projectionQuery'
11
10
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
11
+ import {type ProjectionStoreState} from './types'
12
12
 
13
- vi.mock('../client/clientStore.ts', () => ({getClientState: vi.fn()}))
14
- vi.mock('../resources/createResource', async (importOriginal) => {
15
- const original = await importOriginal<typeof import('../resources/createResource')>()
16
- return {...original, getOrCreateResource: vi.fn()}
17
- })
13
+ vi.mock('../query/queryStore')
18
14
 
19
15
  describe('subscribeToStateAndFetchBatches', () => {
20
- const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
21
-
22
- let state: ResourceState<ProjectionStoreState>
23
- let fetchResults: Subject<{result: ProjectionQueryResult[]; syncTags: SyncTag[]}>
24
- let mockFetch: Mock
16
+ let instance: SanityInstance
17
+ let state: StoreState<ProjectionStoreState>
25
18
 
26
19
  beforeEach(() => {
27
- state = createResourceState<ProjectionStoreState>({
20
+ vi.clearAllMocks()
21
+ instance = createSanityInstance({projectId: 'test', dataset: 'test'})
22
+ state = createStoreState<ProjectionStoreState>({
28
23
  documentProjections: {},
29
- lastLiveEventId: null,
30
24
  subscriptions: {},
31
- syncTags: {},
32
25
  values: {},
33
26
  })
34
- fetchResults = new Subject()
35
-
36
- mockFetch = vi.fn().mockImplementation(
37
- () =>
38
- new Observable((subscriber) => {
39
- const subscription = fetchResults.subscribe({
40
- next: (val) => subscriber.next(val),
41
- complete: () => subscriber.complete(),
42
- })
43
- return () => subscription.unsubscribe()
44
- }),
45
- )
46
- vi.mocked(getClientState).mockReturnValue({
47
- observable: of({observable: {fetch: mockFetch}} as unknown as SanityClient),
48
- } as StateSource<SanityClient>)
27
+
28
+ vi.mocked(getQueryState).mockReturnValue({
29
+ getCurrent: () => undefined,
30
+ observable: NEVER as Observable<ProjectionQueryResult[] | undefined>,
31
+ } as StateSource<ProjectionQueryResult[] | undefined>)
32
+
33
+ vi.mocked(resolveQuery).mockResolvedValue(undefined)
34
+ })
35
+
36
+ afterEach(() => {
37
+ instance.dispose()
49
38
  })
50
39
 
51
40
  it('batches rapid subscription changes into single requests', async () => {
@@ -55,169 +44,267 @@ describe('subscribeToStateAndFetchBatches', () => {
55
44
 
56
45
  // Add multiple subscriptions rapidly
57
46
  state.set('addSubscription1', {
58
- documentProjections: {doc1: projection},
59
- subscriptions: {doc1: {sub1: true}},
47
+ documentProjections: {doc1: {[projectionHash]: projection}},
48
+ // Add projectionHash level to subscriptions
49
+ subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
60
50
  })
61
51
 
62
52
  state.set('addSubscription2', (prev) => ({
63
- documentProjections: {...prev.documentProjections, doc2: projection},
64
- subscriptions: {...prev.subscriptions, doc2: {sub2: true}},
53
+ documentProjections: {
54
+ ...prev.documentProjections,
55
+ doc2: {[projectionHash]: projection},
56
+ },
57
+ // Add projectionHash level to subscriptions
58
+ subscriptions: {
59
+ ...prev.subscriptions,
60
+ doc2: {[projectionHash]: {sub2: true}},
61
+ },
65
62
  }))
66
63
 
67
64
  // Wait for debounce
68
65
  await new Promise((resolve) => setTimeout(resolve, 100))
69
66
 
70
- expect(mockFetch).toHaveBeenCalledTimes(1)
71
- expect(mockFetch.mock.calls[0][1]).toMatchObject({
72
- [`__ids_${projectionHash}`]: ['doc1', 'drafts.doc1', 'doc2', 'drafts.doc2'],
73
- })
67
+ // Should still be 1 call because projections are identical
68
+ expect(getQueryState).toHaveBeenCalledTimes(1)
69
+ expect(getQueryState).toHaveBeenCalledWith(
70
+ instance,
71
+ expect.any(String),
72
+ expect.objectContaining({
73
+ params: {
74
+ [`__ids_${projectionHash}`]: expect.arrayContaining([
75
+ 'doc1',
76
+ 'drafts.doc1',
77
+ 'doc2',
78
+ 'drafts.doc2',
79
+ ]),
80
+ },
81
+ }),
82
+ )
74
83
 
75
84
  subscription.unsubscribe()
76
85
  })
77
86
 
78
- it('always uses latest client/eventId state when fetching', async () => {
87
+ it('processes query results and updates state with resolved values', async () => {
88
+ const teardown = vi.fn()
89
+ const subscriber = vi
90
+ .fn<(observer: Observer<ProjectionQueryResult[] | undefined>) => () => void>()
91
+ .mockReturnValue(teardown)
92
+
93
+ vi.mocked(getQueryState).mockReturnValue({
94
+ getCurrent: () => undefined,
95
+ observable: new Observable(subscriber),
96
+ } as StateSource<ProjectionQueryResult[] | undefined>)
97
+
79
98
  const subscription = subscribeToStateAndFetchBatches({instance, state})
99
+ const projection = '{title}'
100
+ const projectionHash = hashString(projection)
101
+
102
+ expect(subscriber).not.toHaveBeenCalled()
80
103
 
81
104
  // Add a subscription
82
105
  state.set('addSubscription', {
83
- documentProjections: {doc1: '{title, description}'},
84
- subscriptions: {doc1: {sub1: true}},
106
+ documentProjections: {doc1: {[projectionHash]: projection}},
107
+ subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
85
108
  })
86
109
 
87
- // Update lastLiveEventId
88
- state.set('updateEventId', {lastLiveEventId: 'event1'})
110
+ expect(subscriber).not.toHaveBeenCalled()
89
111
 
90
112
  // Wait for debounce
91
113
  await new Promise((resolve) => setTimeout(resolve, 100))
92
114
 
93
- expect(mockFetch).toHaveBeenCalledWith(
94
- expect.any(String),
95
- expect.any(Object),
96
- expect.objectContaining({
97
- lastLiveEventId: 'event1',
98
- }),
99
- )
115
+ expect(subscriber).toHaveBeenCalled()
116
+ expect(teardown).not.toHaveBeenCalled()
117
+
118
+ const [observer] = subscriber.mock.lastCall!
119
+
120
+ const timestamp = new Date().toISOString()
121
+
122
+ observer.next([
123
+ {
124
+ _id: 'doc1',
125
+ _type: 'doc',
126
+ _updatedAt: timestamp,
127
+ result: {title: 'resolved'},
128
+ __projectionHash: projectionHash,
129
+ },
130
+ {
131
+ _id: 'drafts.doc1',
132
+ _type: 'doc',
133
+ _updatedAt: timestamp,
134
+ result: {title: 'resolved'},
135
+ __projectionHash: projectionHash,
136
+ },
137
+ ])
138
+
139
+ const {values} = state.get()
140
+ expect(values['doc1']?.[projectionHash]).toEqual({
141
+ isPending: false,
142
+ data: {
143
+ title: 'resolved',
144
+ status: {
145
+ lastEditedDraftAt: timestamp,
146
+ lastEditedPublishedAt: timestamp,
147
+ },
148
+ },
149
+ })
100
150
 
101
151
  subscription.unsubscribe()
152
+ expect(teardown).toHaveBeenCalled()
102
153
  })
103
154
 
104
155
  it('handles new subscriptions optimistically with pending states', async () => {
156
+ const projection = '{title, description}'
157
+ const projectionHash = hashString(projection)
158
+
105
159
  state.set('initializeValues', {
106
- documentProjections: {doc1: '{title, description}', doc2: '{title, description}'},
107
- values: {doc1: {data: {title: 'Doc 1'}, isPending: false}},
108
- subscriptions: {doc1: {sub1: true}},
160
+ documentProjections: {
161
+ doc1: {[projectionHash]: projection},
162
+ doc2: {[projectionHash]: projection},
163
+ },
164
+ values: {doc1: {[projectionHash]: {data: {title: 'Doc 1'}, isPending: false}}},
165
+ // Add projectionHash level to subscriptions
166
+ subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
109
167
  })
110
168
 
111
169
  const subscription = subscribeToStateAndFetchBatches({instance, state})
112
170
 
113
- // Add a subscription for a document already in the batch
171
+ // Add another subscription for doc1 (same hash)
114
172
  state.set('addSubscriptionAlreadyInBatch', (prev) => ({
115
- subscriptions: {doc1: {sub1: true, ...prev.subscriptions['doc1'], sub2: true}},
173
+ // Only need to update subscriptions here
174
+ subscriptions: {
175
+ ...prev.subscriptions,
176
+ doc1: {
177
+ ...(prev.subscriptions['doc1'] ?? {}),
178
+ [projectionHash]: {
179
+ ...(prev.subscriptions['doc1']?.[projectionHash] ?? {}),
180
+ sub2: true,
181
+ },
182
+ },
183
+ },
116
184
  }))
117
185
 
118
186
  // this isn't a new subscription so it isn't pending by design.
119
187
  // the pending state is intended to only appear for new documents
120
- expect(state.get().values['doc1']).toEqual({data: {title: 'Doc 1'}, isPending: false})
188
+ expect(state.get().values['doc1']?.[projectionHash]).toEqual({
189
+ data: {title: 'Doc 1'},
190
+ isPending: false,
191
+ })
121
192
 
122
193
  expect(state.get().values['doc2']).toBeUndefined()
123
194
 
124
- state.set('addSubscriptionNotInBatch', {
125
- subscriptions: {doc2: {sub1: true}},
126
- })
195
+ // Add subscription for doc2 (same hash)
196
+ state.set('addSubscriptionNotInBatch', (prev) => ({
197
+ // Only need to update subscriptions here
198
+ subscriptions: {
199
+ ...prev.subscriptions,
200
+ doc2: {
201
+ ...(prev.subscriptions['doc2'] ?? {}),
202
+ [projectionHash]: {
203
+ ...(prev.subscriptions['doc2']?.[projectionHash] ?? {}),
204
+ sub1: true,
205
+ },
206
+ },
207
+ },
208
+ }))
127
209
 
128
- await new Promise((resolve) => setTimeout(resolve, 100))
210
+ // Wait for the debounced optimistic update to occur
211
+ await new Promise((resolve) => setTimeout(resolve, 50 + 10)) // Wait slightly longer than debounce (50ms)
129
212
 
130
- expect(state.get().values['doc2']).toEqual({data: null, isPending: true})
213
+ // Check state for doc2 (should now be pending)
214
+ expect(state.get().values['doc2']?.[projectionHash]).toEqual({data: null, isPending: true})
131
215
 
132
216
  subscription.unsubscribe()
133
217
  })
134
218
 
135
219
  it('cancels and restarts fetches when subscription set changes', async () => {
220
+ const abortSpy = vi.spyOn(AbortController.prototype, 'abort')
136
221
  const subscription = subscribeToStateAndFetchBatches({instance, state})
222
+ const projection = '{title, description}'
223
+ const projectionHash = hashString(projection)
224
+ const projection2 = '{_id}' // Different projection
225
+ const projectionHash2 = hashString(projection2)
137
226
 
138
227
  // Add initial subscription
139
228
  state.set('addSubscription1', {
140
- documentProjections: {doc1: '{title, description}'},
141
- subscriptions: {doc1: {sub1: true}},
229
+ documentProjections: {doc1: {[projectionHash]: projection}},
230
+ // Add projectionHash level to subscriptions
231
+ subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
142
232
  })
143
233
 
144
234
  await new Promise((resolve) => setTimeout(resolve, 100))
235
+ const initialQueryCallCount = vi.mocked(getQueryState).mock.calls.length
145
236
 
146
- // Add another subscription before first fetch completes
237
+ // Add another subscription (different doc, different projection) - This should trigger abort + new fetch
147
238
  state.set('addSubscription2', (prev) => ({
148
- documentProjections: {...prev.documentProjections, doc2: '{title, description}'},
149
- subscriptions: {...prev.subscriptions, doc2: {sub2: true}},
239
+ documentProjections: {...prev.documentProjections, doc2: {[projectionHash2]: projection2}},
240
+ // Add projectionHash level to subscriptions
241
+ subscriptions: {
242
+ ...prev.subscriptions,
243
+ doc2: {[projectionHash2]: {sub2: true}},
244
+ },
150
245
  }))
151
246
 
152
247
  await new Promise((resolve) => setTimeout(resolve, 100))
153
248
 
154
- expect(mockFetch).toHaveBeenCalledTimes(2)
249
+ // Expected calls:
250
+ // 1. Initial fetch (doc1, hash1)
251
+ // 2. Adding doc2 subscription (optimistic update, no fetch)
252
+ // 3. Debounced fetch for (doc1, hash1) AND (doc2, hash2)
253
+ expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 1)
254
+ // Abort should have been called because the required projections changed
255
+ expect(abortSpy).toHaveBeenCalled()
155
256
 
156
257
  subscription.unsubscribe()
157
258
  })
158
259
 
159
- it('only refetches when actually needed due to distinctUntilChanged() usage', async () => {
160
- const subscription = subscribeToStateAndFetchBatches({instance, state})
161
-
162
- // Add a subscription
163
- state.set('addSubscription', {
164
- documentProjections: {doc1: '{title, description}'},
165
- subscriptions: {doc1: {sub1: true}},
166
- })
167
-
168
- await new Promise((resolve) => setTimeout(resolve, 100))
169
-
170
- // Update state but don't change subscriptions
171
- state.set('unrelatedChange', {
172
- syncTags: {'s1:tag1': true},
173
- })
174
-
175
- await new Promise((resolve) => setTimeout(resolve, 100))
176
-
177
- expect(mockFetch).toHaveBeenCalledTimes(1)
260
+ it('processes and applies fetch results correctly', async () => {
261
+ const subscriber =
262
+ vi.fn<(observer: Observer<ProjectionQueryResult[] | undefined>) => () => void>()
178
263
 
179
- subscription.unsubscribe()
180
- })
264
+ vi.mocked(getQueryState).mockReturnValue({
265
+ getCurrent: () => undefined,
266
+ observable: new Observable(subscriber),
267
+ } as StateSource<ProjectionQueryResult[] | undefined>)
181
268
 
182
- it('processes and applies fetch results correctly', async () => {
183
269
  const subscription = subscribeToStateAndFetchBatches({instance, state})
270
+ const projection = '{title, description}'
271
+ const projectionHash = hashString(projection)
184
272
 
185
273
  // Add a subscription
186
274
  state.set('addSubscription', {
187
- documentProjections: {doc1: '{title, description}'},
188
- subscriptions: {doc1: {sub1: true}},
275
+ documentProjections: {doc1: {[projectionHash]: projection}},
276
+ // Add projectionHash level to subscriptions
277
+ subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
189
278
  })
190
279
 
191
280
  await new Promise((resolve) => setTimeout(resolve, 100))
192
281
 
282
+ expect(subscriber).toHaveBeenCalled()
283
+ const [observer] = subscriber.mock.lastCall!
284
+
193
285
  // Emit fetch results
194
- fetchResults.next({
195
- result: [
196
- {
197
- _id: 'doc1',
198
- _type: 'test',
199
- _updatedAt: '2024-01-01T00:00:00Z',
200
- result: {title: 'Test Document', description: 'Test Description'},
201
- },
202
- ],
203
- syncTags: ['s1:tag1', 's1:tag2'],
204
- })
286
+ const timestamp = '2024-01-01T00:00:00Z'
287
+ observer.next([
288
+ {
289
+ _id: 'doc1',
290
+ _type: 'test',
291
+ _updatedAt: timestamp,
292
+ result: {title: 'Test Document', description: 'Test Description'},
293
+ __projectionHash: projectionHash,
294
+ },
295
+ ])
205
296
 
206
297
  // Check that the state was updated
207
- expect(state.get().values['doc1']).toEqual({
298
+ expect(state.get().values['doc1']?.[projectionHash]).toEqual({
208
299
  data: expect.objectContaining({
209
300
  title: 'Test Document',
210
301
  description: 'Test Description',
211
302
  status: {
212
- lastEditedPublishedAt: '2024-01-01T00:00:00Z',
303
+ lastEditedPublishedAt: timestamp,
213
304
  },
214
305
  }),
215
306
  isPending: false,
216
307
  })
217
- expect(state.get().syncTags).toEqual({
218
- 's1:tag1': true,
219
- 's1:tag2': true,
220
- })
221
308
 
222
309
  subscription.unsubscribe()
223
310
  })