@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.
- package/dist/index.d.ts +441 -322
- package/dist/index.js +1685 -1481
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
- package/src/_exports/index.ts +32 -30
- package/src/auth/authStore.test.ts +149 -104
- package/src/auth/authStore.ts +51 -100
- package/src/auth/handleAuthCallback.test.ts +67 -34
- package/src/auth/handleAuthCallback.ts +8 -7
- package/src/auth/logout.test.ts +61 -29
- package/src/auth/logout.ts +26 -28
- package/src/auth/refreshStampedToken.test.ts +197 -91
- package/src/auth/refreshStampedToken.ts +170 -59
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
- package/src/client/clientStore.test.ts +131 -67
- package/src/client/clientStore.ts +117 -116
- package/src/comlink/controller/actions/destroyController.test.ts +38 -13
- package/src/comlink/controller/actions/destroyController.ts +11 -15
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
- package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
- package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
- package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
- package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
- package/src/comlink/controller/actions/releaseChannel.ts +22 -21
- package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
- package/src/comlink/controller/comlinkControllerStore.ts +44 -5
- package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
- package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
- package/src/comlink/node/actions/releaseNode.test.ts +75 -55
- package/src/comlink/node/actions/releaseNode.ts +19 -21
- package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
- package/src/comlink/node/comlinkNodeStore.ts +22 -5
- package/src/config/authConfig.ts +79 -0
- package/src/config/sanityConfig.ts +48 -0
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +18 -5
- package/src/document/actions.test.ts +22 -10
- package/src/document/actions.ts +44 -56
- package/src/document/applyDocumentActions.test.ts +96 -36
- package/src/document/applyDocumentActions.ts +140 -99
- package/src/document/documentStore.test.ts +103 -155
- package/src/document/documentStore.ts +247 -238
- package/src/document/listen.ts +56 -55
- package/src/document/patchOperations.ts +0 -43
- package/src/document/permissions.test.ts +25 -12
- package/src/document/permissions.ts +11 -4
- package/src/document/processActions.test.ts +41 -8
- package/src/document/reducers.test.ts +87 -16
- package/src/document/reducers.ts +2 -2
- package/src/document/sharedListener.test.ts +34 -16
- package/src/document/sharedListener.ts +33 -11
- package/src/preview/getPreviewState.test.ts +40 -39
- package/src/preview/getPreviewState.ts +68 -56
- package/src/preview/previewConstants.ts +43 -0
- package/src/preview/previewQuery.test.ts +1 -1
- package/src/preview/previewQuery.ts +4 -5
- package/src/preview/previewStore.test.ts +13 -58
- package/src/preview/previewStore.ts +7 -21
- package/src/preview/resolvePreview.test.ts +33 -104
- package/src/preview/resolvePreview.ts +11 -21
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
- package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
- package/src/preview/util.ts +1 -0
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +28 -5
- package/src/projection/getProjectionState.test.ts +188 -72
- package/src/projection/getProjectionState.ts +92 -62
- package/src/projection/projectionQuery.test.ts +114 -12
- package/src/projection/projectionQuery.ts +75 -32
- package/src/projection/projectionStore.test.ts +13 -51
- package/src/projection/projectionStore.ts +6 -43
- package/src/projection/resolveProjection.test.ts +32 -127
- package/src/projection/resolveProjection.ts +16 -28
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +203 -116
- package/src/projection/subscribeToStateAndFetchBatches.ts +140 -85
- package/src/projection/types.ts +50 -0
- package/src/projection/util.ts +3 -1
- package/src/projects/projects.test.ts +13 -4
- package/src/projects/projects.ts +6 -1
- package/src/query/queryStore.test.ts +10 -47
- package/src/query/queryStore.ts +151 -133
- package/src/query/queryStoreConstants.ts +2 -0
- package/src/store/createActionBinder.test.ts +153 -0
- package/src/store/createActionBinder.ts +176 -0
- package/src/store/createSanityInstance.test.ts +84 -0
- package/src/store/createSanityInstance.ts +124 -0
- package/src/store/createStateSourceAction.test.ts +196 -0
- package/src/store/createStateSourceAction.ts +260 -0
- package/src/store/createStoreInstance.test.ts +81 -0
- package/src/store/createStoreInstance.ts +80 -0
- package/src/store/createStoreState.test.ts +85 -0
- package/src/store/createStoreState.ts +92 -0
- package/src/store/defineStore.test.ts +18 -0
- package/src/store/defineStore.ts +81 -0
- package/src/users/reducers.test.ts +318 -0
- package/src/users/reducers.ts +88 -0
- package/src/users/types.ts +46 -4
- package/src/users/usersConstants.ts +4 -0
- package/src/users/usersStore.test.ts +350 -223
- package/src/users/usersStore.ts +285 -149
- package/src/utils/createFetcherStore.test.ts +6 -7
- package/src/utils/createFetcherStore.ts +150 -153
- package/src/utils/createGroqSearchFilter.test.ts +75 -0
- package/src/utils/createGroqSearchFilter.ts +85 -0
- package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
- package/dist/index.cjs +0 -4888
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2121
- package/src/auth/fetchLoginUrls.test.ts +0 -163
- package/src/auth/fetchLoginUrls.ts +0 -74
- package/src/common/createLiveEventSubscriber.test.ts +0 -121
- package/src/common/createLiveEventSubscriber.ts +0 -55
- package/src/common/types.ts +0 -4
- package/src/instance/identity.test.ts +0 -46
- package/src/instance/identity.ts +0 -29
- package/src/instance/sanityInstance.test.ts +0 -77
- package/src/instance/sanityInstance.ts +0 -57
- package/src/instance/types.ts +0 -37
- package/src/preview/getPreviewProjection.ts +0 -45
- package/src/resources/README.md +0 -370
- package/src/resources/createAction.test.ts +0 -101
- package/src/resources/createAction.ts +0 -44
- package/src/resources/createResource.test.ts +0 -112
- package/src/resources/createResource.ts +0 -102
- package/src/resources/createStateSourceAction.test.ts +0 -114
- package/src/resources/createStateSourceAction.ts +0 -83
- package/src/resources/createStore.test.ts +0 -67
- package/src/resources/createStore.ts +0 -46
- package/src/store/createStore.test.ts +0 -108
- package/src/store/createStore.ts +0 -106
- /package/src/{common/util.ts → utils/hashString.ts} +0 -0
|
@@ -1,144 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 {
|
|
2
|
-
|
|
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
|
-
|
|
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 =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {type
|
|
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('../
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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: {
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
expect(
|
|
72
|
-
|
|
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('
|
|
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:
|
|
84
|
-
subscriptions: {doc1: {sub1: true}},
|
|
106
|
+
documentProjections: {doc1: {[projectionHash]: projection}},
|
|
107
|
+
subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
|
|
85
108
|
})
|
|
86
109
|
|
|
87
|
-
|
|
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(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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: {
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
171
|
+
// Add another subscription for doc1 (same hash)
|
|
114
172
|
state.set('addSubscriptionAlreadyInBatch', (prev) => ({
|
|
115
|
-
|
|
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({
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
141
|
-
|
|
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
|
|
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:
|
|
149
|
-
|
|
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
|
-
|
|
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('
|
|
160
|
-
const
|
|
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
|
-
|
|
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:
|
|
188
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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:
|
|
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
|
})
|