@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
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/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {type SanityConfig} from '../config/sanityConfig'
|
|
2
2
|
import {insecureRandomId} from '../utils/ids'
|
|
3
3
|
import {createLogger, type InstanceContext} from '../utils/logger'
|
|
4
|
+
import {pickProperties} from '../utils/object'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Represents a Sanity.io resource instance with its own configuration and lifecycle
|
|
7
|
+
* Represents a Sanity.io resource instance with its own configuration and lifecycle
|
|
7
8
|
*
|
|
8
9
|
* @public
|
|
9
10
|
*/
|
|
@@ -14,7 +15,9 @@ export interface SanityInstance {
|
|
|
14
15
|
*/
|
|
15
16
|
readonly instanceId: string
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Resolved configuration for this instance
|
|
20
|
+
*/
|
|
18
21
|
readonly config: SanityConfig
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -35,12 +38,36 @@ export interface SanityInstance {
|
|
|
35
38
|
* @returns Function to unsubscribe the callback
|
|
36
39
|
*/
|
|
37
40
|
onDispose(cb: () => void): () => void
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Gets the parent instance in the hierarchy
|
|
44
|
+
* @returns Parent instance or undefined if this is the root
|
|
45
|
+
* @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
|
|
46
|
+
*/
|
|
47
|
+
getParent(): SanityInstance | undefined
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a child instance with merged configuration
|
|
51
|
+
* @param config - Configuration to merge with parent values
|
|
52
|
+
* @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
|
|
53
|
+
*/
|
|
54
|
+
createChild(config: SanityConfig): SanityInstance
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Traverses the instance hierarchy to find the first instance whose configuration
|
|
58
|
+
* matches the given target config using a shallow comparison.
|
|
59
|
+
* @param targetConfig - A partial configuration object containing key-value pairs to match.
|
|
60
|
+
* @returns The first matching instance or undefined if no match is found.
|
|
61
|
+
* @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
|
|
62
|
+
*/
|
|
63
|
+
match(targetConfig: Partial<SanityConfig>): SanityInstance | undefined
|
|
38
64
|
}
|
|
39
65
|
|
|
40
66
|
/**
|
|
41
67
|
* Creates a new Sanity resource instance
|
|
42
68
|
* @param config - Configuration for the instance (optional)
|
|
43
69
|
* @returns A configured SanityInstance
|
|
70
|
+
* @remarks When creating child instances, configurations are merged with parent values
|
|
44
71
|
*
|
|
45
72
|
* @public
|
|
46
73
|
*/
|
|
@@ -49,21 +76,32 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
|
|
|
49
76
|
const disposeListeners = new Map<string, () => void>()
|
|
50
77
|
const disposed = {current: false}
|
|
51
78
|
|
|
79
|
+
// Create instance context for logging
|
|
52
80
|
const instanceContext: InstanceContext = {
|
|
53
81
|
instanceId,
|
|
82
|
+
projectId: config.projectId,
|
|
83
|
+
dataset: config.dataset,
|
|
54
84
|
}
|
|
55
85
|
|
|
86
|
+
// Create logger with instance context
|
|
56
87
|
const logger = createLogger('sdk', {instanceContext})
|
|
57
88
|
|
|
89
|
+
// Log instance creation
|
|
58
90
|
logger.info('Sanity instance created', {
|
|
91
|
+
hasProjectId: !!config.projectId,
|
|
92
|
+
hasDataset: !!config.dataset,
|
|
59
93
|
hasAuth: !!config.auth,
|
|
60
94
|
hasPerspective: !!config.perspective,
|
|
61
95
|
})
|
|
62
96
|
|
|
97
|
+
// Log configuration details at debug level
|
|
63
98
|
logger.debug('Instance configuration', {
|
|
99
|
+
projectId: config.projectId,
|
|
100
|
+
dataset: config.dataset,
|
|
64
101
|
perspective: config.perspective,
|
|
65
102
|
hasStudioConfig: !!config.studio,
|
|
66
103
|
hasStudioTokenSource: !!config.studio?.auth?.token,
|
|
104
|
+
legacyStudioMode: config.studioMode?.enabled,
|
|
67
105
|
hasAuthProviders: !!config.auth?.providers,
|
|
68
106
|
hasAuthToken: !!config.auth?.token,
|
|
69
107
|
})
|
|
@@ -93,6 +131,43 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
|
|
|
93
131
|
disposeListeners.delete(listenerId)
|
|
94
132
|
}
|
|
95
133
|
},
|
|
134
|
+
getParent: () => undefined,
|
|
135
|
+
createChild: (next) => {
|
|
136
|
+
logger.debug('Creating child instance', {
|
|
137
|
+
parentInstanceId: instanceId.slice(0, 8),
|
|
138
|
+
overridingProjectId: !!next.projectId,
|
|
139
|
+
overridingDataset: !!next.dataset,
|
|
140
|
+
overridingAuth: !!next.auth,
|
|
141
|
+
})
|
|
142
|
+
const child = Object.assign(
|
|
143
|
+
createSanityInstance({
|
|
144
|
+
...config,
|
|
145
|
+
...next,
|
|
146
|
+
...(config.auth === next.auth
|
|
147
|
+
? config.auth
|
|
148
|
+
: config.auth && next.auth && {auth: {...config.auth, ...next.auth}}),
|
|
149
|
+
}),
|
|
150
|
+
{getParent: () => instance},
|
|
151
|
+
)
|
|
152
|
+
logger.trace('Child instance created', {
|
|
153
|
+
internal: true,
|
|
154
|
+
childInstanceId: child.instanceId.slice(0, 8),
|
|
155
|
+
})
|
|
156
|
+
return child
|
|
157
|
+
},
|
|
158
|
+
match: (targetConfig) => {
|
|
159
|
+
if (
|
|
160
|
+
Object.entries(pickProperties(targetConfig, ['auth', 'projectId', 'dataset'])).every(
|
|
161
|
+
([key, value]) => config[key as keyof SanityConfig] === value,
|
|
162
|
+
)
|
|
163
|
+
) {
|
|
164
|
+
return instance
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parent = instance.getParent()
|
|
168
|
+
if (parent) return parent.match(targetConfig)
|
|
169
|
+
return undefined
|
|
170
|
+
},
|
|
96
171
|
}
|
|
97
172
|
|
|
98
173
|
return instance
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {filter, firstValueFrom, timeout} from 'rxjs'
|
|
1
2
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
3
|
|
|
3
4
|
import {createSanityInstance, type SanityInstance} from './createSanityInstance'
|
|
@@ -14,7 +15,7 @@ describe('createStateSourceAction', () => {
|
|
|
14
15
|
let instance: SanityInstance
|
|
15
16
|
|
|
16
17
|
beforeEach(() => {
|
|
17
|
-
instance = createSanityInstance()
|
|
18
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
18
19
|
state = createStoreState({count: 0, items: [] as string[]}, {name: 'test-store'})
|
|
19
20
|
})
|
|
20
21
|
|
|
@@ -179,7 +180,7 @@ describe('createStateSourceAction', () => {
|
|
|
179
180
|
|
|
180
181
|
// New test: distinct contexts for same state with different instance
|
|
181
182
|
it('should create distinct contexts for same state with different instance', () => {
|
|
182
|
-
const secondInstance = createSanityInstance()
|
|
183
|
+
const secondInstance = createSanityInstance({projectId: 'test2', dataset: 'test2'})
|
|
183
184
|
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
184
185
|
|
|
185
186
|
const source1 = createStateSourceAction(selector)({state, instance, key: null})
|
|
@@ -194,4 +195,65 @@ describe('createStateSourceAction', () => {
|
|
|
194
195
|
expect(context1.instance).toBe(instance)
|
|
195
196
|
expect(context2.instance).toBe(secondInstance)
|
|
196
197
|
})
|
|
198
|
+
|
|
199
|
+
it('correctly observes values defined in the onSubscribe', async () => {
|
|
200
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
201
|
+
const source = createStateSourceAction({
|
|
202
|
+
selector: selector,
|
|
203
|
+
onSubscribe() {
|
|
204
|
+
state.set('update', {count: 1})
|
|
205
|
+
},
|
|
206
|
+
})({state, instance, key: null})
|
|
207
|
+
|
|
208
|
+
const value = await firstValueFrom(
|
|
209
|
+
source.observable.pipe(
|
|
210
|
+
filter((i) => i === 1),
|
|
211
|
+
timeout(10),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
expect(value).toBe(1)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('only invokes selector once on changes', () => {
|
|
218
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
219
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
220
|
+
|
|
221
|
+
expect(selector).toBeCalledTimes(0)
|
|
222
|
+
|
|
223
|
+
// Now it should be called once:
|
|
224
|
+
const sub = source.observable.subscribe()
|
|
225
|
+
expect(selector).toBeCalledTimes(1)
|
|
226
|
+
|
|
227
|
+
// The observable should be shared so this shouldn't invoke it first.
|
|
228
|
+
const sub2 = source.observable.subscribe()
|
|
229
|
+
expect(selector).toBeCalledTimes(1)
|
|
230
|
+
|
|
231
|
+
// Updating the value should only invoke it once:
|
|
232
|
+
state.set('update', {count: 1})
|
|
233
|
+
expect(selector).toBeCalledTimes(2)
|
|
234
|
+
|
|
235
|
+
sub2.unsubscribe()
|
|
236
|
+
sub.unsubscribe()
|
|
237
|
+
|
|
238
|
+
// Once everyone has unsubscribed it should be invoked again.
|
|
239
|
+
const sub3 = source.observable.subscribe()
|
|
240
|
+
expect(selector).toBeCalledTimes(3)
|
|
241
|
+
sub3.unsubscribe()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('only subscribes once when mixing subscribe/observable', () => {
|
|
245
|
+
const selector = vi.fn(({state: s}: SelectorContext<CountStoreState>) => s.count)
|
|
246
|
+
const source = createStateSourceAction({selector: selector})({state, instance, key: null})
|
|
247
|
+
|
|
248
|
+
expect(selector).toBeCalledTimes(0)
|
|
249
|
+
|
|
250
|
+
const sub = source.observable.subscribe()
|
|
251
|
+
expect(selector).toBeCalledTimes(1)
|
|
252
|
+
|
|
253
|
+
const sub2 = source.subscribe()
|
|
254
|
+
expect(selector).toBeCalledTimes(1)
|
|
255
|
+
|
|
256
|
+
sub.unsubscribe()
|
|
257
|
+
sub2()
|
|
258
|
+
})
|
|
197
259
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {distinctUntilChanged, map, Observable,
|
|
1
|
+
import {defer, distinctUntilChanged, finalize, map, Observable, shareReplay, skip} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {type StoreAction} from './createActionBinder'
|
|
4
4
|
import {type SanityInstance} from './createSanityInstance'
|
|
@@ -57,9 +57,9 @@ export interface StateSource<T> {
|
|
|
57
57
|
* ```ts
|
|
58
58
|
* // Using both state and instance in a selector (psuedo example)
|
|
59
59
|
* const getUserByProjectId = createStateSourceAction(
|
|
60
|
-
* ({ state, instance }: SelectorContext<UsersState>, options?:
|
|
60
|
+
* ({ state, instance }: SelectorContext<UsersState>, options?: ProjectHandle) => {
|
|
61
61
|
* const allUsers = state.users
|
|
62
|
-
* const projectId = options?.projectId ?? instance.config.
|
|
62
|
+
* const projectId = options?.projectId ?? instance.config.projectId
|
|
63
63
|
* return allUsers.filter(user => user.projectId === projectId)
|
|
64
64
|
* }
|
|
65
65
|
* )
|
|
@@ -155,13 +155,13 @@ interface StateSourceOptions<TState, TParams extends unknown[], TReturn, TKey> {
|
|
|
155
155
|
* // ...
|
|
156
156
|
* })
|
|
157
157
|
*
|
|
158
|
-
* const getDocument =
|
|
158
|
+
* const getDocument = bindActionByDataset(
|
|
159
159
|
* documentStore,
|
|
160
|
-
* createStateSourceAction(({state},
|
|
160
|
+
* createStateSourceAction(({state}, documentId: string) => state.documents[documentId])
|
|
161
161
|
* )
|
|
162
162
|
*
|
|
163
163
|
* // Usage
|
|
164
|
-
* const documentSource = getDocument(sanityInstance,
|
|
164
|
+
* const documentSource = getDocument(sanityInstance, 'doc123')
|
|
165
165
|
* const doc = documentSource.getCurrent()
|
|
166
166
|
* const subscription = documentSource.observable.subscribe(updatedDoc => {
|
|
167
167
|
* console.log('Document changed:', updatedDoc)
|
|
@@ -187,8 +187,7 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
187
187
|
function stateSourceAction(context: StoreContext<TState, TKey>, ...params: TParams) {
|
|
188
188
|
const {state, instance} = context
|
|
189
189
|
|
|
190
|
-
const getCurrent = () => {
|
|
191
|
-
const currentState = state.get()
|
|
190
|
+
const getCurrent = (currentState: TState) => {
|
|
192
191
|
if (typeof currentState !== 'object' || currentState === null) {
|
|
193
192
|
throw new Error(
|
|
194
193
|
`Expected store state to be an object but got "${typeof currentState}" instead`,
|
|
@@ -208,53 +207,49 @@ export function createStateSourceAction<TState, TParams extends unknown[], TRetu
|
|
|
208
207
|
return selector(selectorContext, ...params)
|
|
209
208
|
}
|
|
210
209
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const cleanup = subscribeHandler?.(context, ...params)
|
|
210
|
+
// `state.observable` will emit the current value immediately and
|
|
211
|
+
// hence we inherit the same behavior here.
|
|
212
|
+
let values = state.observable.pipe(map(getCurrent), distinctUntilChanged(isEqual))
|
|
215
213
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
if (subscribeHandler) {
|
|
215
|
+
values = withSubscribeHook(values, () => subscribeHandler(context, ...params))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Share but replay the latest value so every subscriber gets an
|
|
219
|
+
// initial synchronous emission, matching `state.observable`. That keeps
|
|
220
|
+
// `skip(1)` in `subscribe()` aligned with "skip current snapshot" rather than
|
|
221
|
+
// silently eating the first real update after multicasting.
|
|
222
|
+
const sharedValues = values.pipe(shareReplay({bufferSize: 1, refCount: true}))
|
|
223
|
+
|
|
224
|
+
const subscribe = (onStoreChanged?: () => void) => {
|
|
225
|
+
const subscription = sharedValues.pipe(skip(1)).subscribe({
|
|
226
|
+
next: () => onStoreChanged?.(),
|
|
227
|
+
// Propagate selector errors to both subscription types
|
|
228
|
+
error: () => onStoreChanged?.(),
|
|
229
|
+
})
|
|
231
230
|
|
|
232
231
|
return () => {
|
|
233
232
|
subscription.unsubscribe()
|
|
234
|
-
cleanup?.()
|
|
235
233
|
}
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
// Create shared observable that handles multiple subscribers efficiently
|
|
239
|
-
const observable = new Observable<TReturn>((observer) => {
|
|
240
|
-
const emitCurrent = () => {
|
|
241
|
-
try {
|
|
242
|
-
observer.next(getCurrent())
|
|
243
|
-
} catch (error) {
|
|
244
|
-
observer.error(error)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Emit immediately on subscription
|
|
248
|
-
emitCurrent()
|
|
249
|
-
return subscribe(emitCurrent)
|
|
250
|
-
}).pipe(share())
|
|
251
|
-
|
|
252
236
|
return {
|
|
253
|
-
getCurrent,
|
|
237
|
+
getCurrent: () => getCurrent(state.get()),
|
|
254
238
|
subscribe,
|
|
255
|
-
observable,
|
|
239
|
+
observable: sharedValues,
|
|
256
240
|
}
|
|
257
241
|
}
|
|
258
242
|
|
|
259
243
|
return stateSourceAction
|
|
260
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Creates a new Observable which wraps an existing Observable which will invoke
|
|
248
|
+
* the function when a new subscriber appears.
|
|
249
|
+
*/
|
|
250
|
+
function withSubscribeHook<T>(obs: Observable<T>, fn: () => void | (() => void)): Observable<T> {
|
|
251
|
+
return defer(() => {
|
|
252
|
+
const cleanup = fn()
|
|
253
|
+
return cleanup ? obs.pipe(finalize(() => cleanup())) : obs
|
|
254
|
+
})
|
|
255
|
+
}
|
|
@@ -13,12 +13,14 @@ describe('createStoreInstance', () => {
|
|
|
13
13
|
randomUUID: () => 'test-uuid-1234',
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
instance = createSanityInstance()
|
|
16
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
const storeDef: StoreDefinition<{count: number}> = {
|
|
20
20
|
name: 'TestStore',
|
|
21
|
-
getInitialState: () => ({
|
|
21
|
+
getInitialState: (inst) => ({
|
|
22
|
+
count: inst.config.projectId === 'test' ? 0 : -1,
|
|
23
|
+
}),
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
it('should create store instance with initial state', () => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {of} from 'rxjs'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {type SanityInstance} from './createSanityInstance'
|
|
5
|
+
import {
|
|
6
|
+
defineFetcher,
|
|
7
|
+
defineMutation,
|
|
8
|
+
type Fetcher,
|
|
9
|
+
type FetcherSnapshot,
|
|
10
|
+
type MutationResult,
|
|
11
|
+
} from './fetcherStore'
|
|
12
|
+
|
|
13
|
+
declare const instance: SanityInstance
|
|
14
|
+
|
|
15
|
+
interface Widget {
|
|
16
|
+
id: string
|
|
17
|
+
label: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// No explicit generics anywhere below: `TData` is inferred from `fetch`'s
|
|
21
|
+
// `Observable<T>` and `TParams` from the annotated `getKey`/`fetch` params —
|
|
22
|
+
// the whole handle is inferred from the definition, TanStack-style.
|
|
23
|
+
|
|
24
|
+
// A list fetcher: no params, resolves to Widget[].
|
|
25
|
+
const widgets = defineFetcher({
|
|
26
|
+
name: 'test-d-widgets',
|
|
27
|
+
getKey: () => 'all',
|
|
28
|
+
fetch: () => () => of<Widget[]>([]),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// A detail fetcher: one string param, resolves to Widget.
|
|
32
|
+
const widget = defineFetcher({
|
|
33
|
+
name: 'test-d-widget',
|
|
34
|
+
getKey: (_i, id) => id,
|
|
35
|
+
fetch: () => (id: string) => of<Widget>({id, label: ''}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('the handle is inferred from the definition — no explicit generics', () => {
|
|
39
|
+
expectTypeOf(widgets).toEqualTypeOf<Fetcher<[], Widget[]>>()
|
|
40
|
+
expectTypeOf(widget).toEqualTypeOf<Fetcher<[id: string], Widget>>()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('unannotated getKey params are inferred (not any) from the fetcher', () => {
|
|
44
|
+
defineFetcher({
|
|
45
|
+
name: 'test-d-getkey-infer',
|
|
46
|
+
// `id` is not annotated here — it must be inferred as `string` from `fetch`.
|
|
47
|
+
getKey: (_i, id) => {
|
|
48
|
+
expectTypeOf(id).toEqualTypeOf<string>()
|
|
49
|
+
return id
|
|
50
|
+
},
|
|
51
|
+
fetch: () => (id: string) => of<Widget>({id, label: ''}),
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('getState carries TData through the snapshot envelope (not unknown)', () => {
|
|
56
|
+
expectTypeOf(widgets.getState(instance).getCurrent()).toEqualTypeOf<FetcherSnapshot<Widget[]>>()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('the snapshot discriminates data by status', () => {
|
|
60
|
+
const snap = widget.getState(instance, 'w1').getCurrent()
|
|
61
|
+
if (snap.status === 'success') {
|
|
62
|
+
expectTypeOf(snap.data).toEqualTypeOf<Widget>()
|
|
63
|
+
expectTypeOf(snap.dataUpdatedAt).toEqualTypeOf<number>()
|
|
64
|
+
}
|
|
65
|
+
if (snap.status === 'pending') {
|
|
66
|
+
expectTypeOf(snap.data).toEqualTypeOf<undefined>()
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('resolveState and refetch resolve to TData', () => {
|
|
71
|
+
expectTypeOf(widgets.resolveState(instance)).resolves.toEqualTypeOf<Widget[]>()
|
|
72
|
+
expectTypeOf(widget.refetch(instance, 'w1')).resolves.toEqualTypeOf<Widget>()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('the params tuple is enforced per fetcher', () => {
|
|
76
|
+
// @ts-expect-error — detail fetcher requires an id
|
|
77
|
+
void widget.getState(instance)
|
|
78
|
+
// @ts-expect-error — id must be a string
|
|
79
|
+
void widget.getState(instance, 123)
|
|
80
|
+
// @ts-expect-error — list fetcher takes no params
|
|
81
|
+
void widgets.getState(instance, 'nope')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('setData updater is typed to TData', () => {
|
|
85
|
+
widget.setData(instance, ['w1'], {id: 'w1', label: 'x'})
|
|
86
|
+
widget.setData(instance, ['w1'], (current) => {
|
|
87
|
+
expectTypeOf(current).toEqualTypeOf<Widget | undefined>()
|
|
88
|
+
return {id: 'w1', label: 'y'}
|
|
89
|
+
})
|
|
90
|
+
// @ts-expect-error — value must be a full Widget
|
|
91
|
+
widget.setData(instance, ['w1'], {id: 'w1'})
|
|
92
|
+
// @ts-expect-error — updater function must return TData
|
|
93
|
+
widget.setData(instance, ['w1'], () => 42)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('defineMutation resolves to a MutationResult of TResult with a typed input', () => {
|
|
97
|
+
const createWidget = defineMutation<{label: string}, Widget>({
|
|
98
|
+
name: 'test-d-create-widget',
|
|
99
|
+
mutationFn: () => (input) => {
|
|
100
|
+
expectTypeOf(input).toEqualTypeOf<{label: string}>()
|
|
101
|
+
return of<Widget>({id: 'x', label: input.label})
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
expectTypeOf(createWidget(instance, {label: 'a'})).resolves.toEqualTypeOf<
|
|
105
|
+
MutationResult<Widget>
|
|
106
|
+
>()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('CacheWriter enforces the target fetcher’s TData and TParams', () => {
|
|
110
|
+
defineMutation<void, void>({
|
|
111
|
+
name: 'test-d-write-through',
|
|
112
|
+
mutationFn: () => () => of(undefined),
|
|
113
|
+
onMutate: (write) => {
|
|
114
|
+
write(widget, ['w1'], {id: 'w1', label: 'ok'})
|
|
115
|
+
// @ts-expect-error — value must match the fetcher's TData (Widget)
|
|
116
|
+
write(widget, ['w1'], {nope: true})
|
|
117
|
+
// @ts-expect-error — params must match the fetcher's TParams ([string])
|
|
118
|
+
write(widget, [], {id: 'w1', label: 'ok'})
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
})
|