Skip to content

Commit 90f2bca

Browse files
committed
feat: only use a single SanityInstance
1 parent 2d74286 commit 90f2bca

File tree

14 files changed

+179
-324
lines changed

14 files changed

+179
-324
lines changed

packages/core/src/_exports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export {
5050
createProjectHandle,
5151
} from '../config/handles'
5252
export {
53+
childSourceFor,
5354
type DatasetHandle,
5455
type DocumentHandle,
5556
type DocumentSource,

packages/core/src/config/sanityConfig.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,20 @@ export const __sourceData = Symbol('Sanity.DocumentSource')
110110
export function sourceFor(data: {projectId: string; dataset: string}): DocumentSource {
111111
return {[__sourceData]: data}
112112
}
113+
114+
/**
115+
* @public
116+
*/
117+
export function childSourceFor(
118+
parent: DocumentSource,
119+
data: {projectId?: string; dataset?: string},
120+
): DocumentSource {
121+
const parentData = parent[__sourceData]
122+
123+
return {
124+
[__sourceData]: {
125+
projectId: data.projectId ?? parentData.projectId,
126+
dataset: data.dataset ?? parentData.dataset,
127+
},
128+
}
129+
}

packages/core/src/document/applyDocumentActions.test.ts

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -149,61 +149,4 @@ describe('applyDocumentActions', () => {
149149

150150
await expect(applyPromise).rejects.toThrow('Simulated error')
151151
})
152-
153-
it('matches parent instance via child when action projectId and dataset do not match child config', async () => {
154-
// Create a parent instance
155-
const parentInstance = createSanityInstance()
156-
// Create a child instance with different config
157-
const childInstance = parentInstance.createChild({projectId: 'child-p', dataset: 'child-d'})
158-
// Use the child instance in context
159-
// Create an action that refers to the parent's configuration
160-
const action: DocumentAction = {
161-
type: 'document.edit',
162-
documentId: 'doc1',
163-
documentType: 'example',
164-
patches: [{set: {foo: 'childTest'}}],
165-
projectId: 'p',
166-
dataset: 'd',
167-
}
168-
// Call applyDocumentActions with the context using childInstance, but with action requiring parent's config
169-
const applyPromise = applyDocumentActions(childInstance, {
170-
actions: [action],
171-
transactionId: 'txn-child-match',
172-
source,
173-
})
174-
175-
// Simulate an applied transaction on the parent's instance
176-
const appliedTx: AppliedTransaction = {
177-
transactionId: 'txn-child-match',
178-
actions: [action],
179-
disableBatching: false,
180-
outgoingActions: [],
181-
outgoingMutations: [],
182-
base: {doc1: {...exampleDoc, _id: 'doc1', foo: 'old', _rev: 'rev-old'}},
183-
working: {doc1: {...exampleDoc, _id: 'doc1', foo: 'childTest', _rev: 'rev-new'}},
184-
previous: {doc1: {...exampleDoc, _id: 'doc1', foo: 'old', _rev: 'rev-old'}},
185-
previousRevs: {doc1: 'rev-old'},
186-
timestamp: new Date().toISOString(),
187-
}
188-
state.set('simulateApplied', {applied: [appliedTx]})
189-
190-
const result = await applyPromise
191-
expect(result.transactionId).toEqual('txn-child-match')
192-
expect(result.documents).toEqual(appliedTx.working)
193-
expect(result.previous).toEqual(appliedTx.previous)
194-
expect(result.previousRevs).toEqual(appliedTx.previousRevs)
195-
196-
const acceptedResult = {transactionId: 'accepted-child'}
197-
const acceptedEvent: DocumentEvent = {
198-
type: 'accepted',
199-
outgoing: {batchedTransactionIds: ['txn-child-match']} as OutgoingTransaction,
200-
result: acceptedResult,
201-
}
202-
eventsSubject.next(acceptedEvent)
203-
const submittedResult = await result.submitted()
204-
expect(submittedResult).toEqual(acceptedResult)
205-
206-
childInstance.dispose()
207-
parentInstance.dispose()
208-
})
209152
})

packages/core/src/store/createSanityInstance.test.ts

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -27,58 +27,4 @@ describe('createSanityInstance', () => {
2727
instance.dispose()
2828
expect(callback).toHaveBeenCalledTimes(1)
2929
})
30-
31-
it('should create a child instance with merged config and correct parent', () => {
32-
const parent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
33-
const child = parent.createChild({dataset: 'ds2'})
34-
expect(child.config).toEqual({projectId: 'proj1', dataset: 'ds2'})
35-
expect(child.getParent()).toBe(parent)
36-
})
37-
38-
it('should match an instance in the hierarchy using match', () => {
39-
// three-level hierarchy
40-
const grandparent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
41-
const parent = grandparent.createChild({projectId: 'proj2'})
42-
const child = parent.createChild({dataset: 'ds2'})
43-
44-
expect(child.config).toEqual({projectId: 'proj2', dataset: 'ds2'})
45-
expect(parent.config).toEqual({projectId: 'proj2', dataset: 'ds1'})
46-
47-
expect(child.match({dataset: 'ds2'})).toBe(child)
48-
expect(child.match({projectId: 'proj2'})).toBe(child)
49-
expect(child.match({projectId: 'proj1'})).toBe(grandparent)
50-
expect(parent.match({projectId: 'proj1'})).toBe(grandparent)
51-
expect(grandparent.match({projectId: 'proj1'})).toBe(grandparent)
52-
})
53-
54-
it('should match `undefined` when the desired resource ID should not be set on an instance', () => {
55-
const noProjectOrDataset = createSanityInstance()
56-
const noDataset = noProjectOrDataset.createChild({projectId: 'proj1'})
57-
const leaf = noDataset.createChild({dataset: 'ds1'})
58-
59-
// no keys means anything (in this case, self) will match
60-
expect(leaf.match({})).toBe(leaf)
61-
62-
// `[resourceId]: undefined` means match an instance with no dataset set
63-
expect(leaf.match({dataset: undefined})).toBe(noDataset)
64-
expect(noDataset.match({dataset: undefined})).toBe(noDataset)
65-
expect(leaf.match({projectId: undefined})).toBe(noProjectOrDataset)
66-
expect(noDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
67-
expect(noProjectOrDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
68-
})
69-
70-
it('should return undefined when no match is found', () => {
71-
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
72-
expect(instance.match({dataset: 'non-existent'})).toBeUndefined()
73-
})
74-
75-
it('should inherit and merge auth config', () => {
76-
const parent = createSanityInstance({
77-
projectId: 'proj1',
78-
dataset: 'ds1',
79-
auth: {apiHost: 'api.sanity.work'},
80-
})
81-
const child = parent.createChild({auth: {token: 'my-token'}})
82-
expect(child.config.auth).toEqual({apiHost: 'api.sanity.work', token: 'my-token'})
83-
})
8430
})

packages/core/src/store/createSanityInstance.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import {pick} from 'lodash-es'
2-
31
import {type SanityConfig} from '../config/sanityConfig'
42
import {insecureRandomId} from '../utils/ids'
53

@@ -40,27 +38,6 @@ export interface SanityInstance {
4038
* @returns Function to unsubscribe the callback
4139
*/
4240
onDispose(cb: () => void): () => void
43-
44-
/**
45-
* Gets the parent instance in the hierarchy
46-
* @returns Parent instance or undefined if this is the root
47-
*/
48-
getParent(): SanityInstance | undefined
49-
50-
/**
51-
* Creates a child instance with merged configuration
52-
* @param config - Configuration to merge with parent values
53-
* @remarks Child instances inherit parent configuration but can override values
54-
*/
55-
createChild(config: SanityConfig): SanityInstance
56-
57-
/**
58-
* Traverses the instance hierarchy to find the first instance whose configuration
59-
* matches the given target config using a shallow comparison.
60-
* @param targetConfig - A partial configuration object containing key-value pairs to match.
61-
* @returns The first matching instance or undefined if no match is found.
62-
*/
63-
match(targetConfig: Partial<SanityConfig>): SanityInstance | undefined
6441
}
6542

6643
/**
@@ -93,31 +70,6 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
9370
disposeListeners.delete(listenerId)
9471
}
9572
},
96-
getParent: () => undefined,
97-
createChild: (next) =>
98-
Object.assign(
99-
createSanityInstance({
100-
...config,
101-
...next,
102-
...(config.auth === next.auth
103-
? config.auth
104-
: config.auth && next.auth && {auth: {...config.auth, ...next.auth}}),
105-
}),
106-
{getParent: () => instance},
107-
),
108-
match: (targetConfig) => {
109-
if (
110-
Object.entries(pick(targetConfig, 'auth', 'projectId', 'dataset')).every(
111-
([key, value]) => config[key as keyof SanityConfig] === value,
112-
)
113-
) {
114-
return instance
115-
}
116-
117-
const parent = instance.getParent()
118-
if (parent) return parent.match(targetConfig)
119-
return undefined
120-
},
12173
}
12274

12375
return instance

packages/core/src/users/reducers.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ describe('Users Reducers', () => {
2525
isDisposed: () => false,
2626
dispose: () => {},
2727
onDispose: () => () => {},
28-
getParent: () => undefined,
29-
createChild: (_config) => mockInstance,
30-
match: () => undefined,
3128
}
3229

3330
const sampleOptions: GetUsersOptions = {
Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import {type SanityConfig} from '@sanity/sdk'
2-
import {type ReactElement, type ReactNode} from 'react'
1+
import {createSanityInstance, type SanityConfig, type SanityInstance, sourceFor} from '@sanity/sdk'
2+
import {type ReactElement, type ReactNode, Suspense, useEffect, useMemo, useRef} from 'react'
33

4-
import {ResourceProvider} from '../context/ResourceProvider'
4+
import {DefaultSourceContext} from '../context/DefaultSourceContext'
5+
import {PerspectiveContext} from '../context/PerspectiveContext'
6+
import {SanityInstanceContext} from '../context/SanityInstanceContext'
57
import {AuthBoundary, type AuthBoundaryProps} from './auth/AuthBoundary'
68

79
/**
@@ -26,27 +28,67 @@ export function SDKProvider({
2628
fallback,
2729
...props
2830
}: SDKProviderProps): ReactElement {
29-
// reverse because we want the first config to be the default, but the
30-
// ResourceProvider nesting makes the last one the default
31-
const configs = (Array.isArray(config) ? config : [config]).slice().reverse()
32-
const projectIds = configs.map((c) => c.projectId).filter((id): id is string => !!id)
33-
34-
// Create a nested structure of ResourceProviders for each config
35-
const createNestedProviders = (index: number): ReactElement => {
36-
if (index >= configs.length) {
37-
return (
38-
<AuthBoundary {...props} projectIds={projectIds}>
39-
{children}
40-
</AuthBoundary>
41-
)
31+
if (Array.isArray(config)) {
32+
// eslint-disable-next-line no-console
33+
console.warn(
34+
'<SDKProvider>: Multiple configs are no longer supported. Only the first one will be used.',
35+
)
36+
}
37+
38+
const {projectId, dataset, perspective, ...mainConfig} = Array.isArray(config)
39+
? config[0] || {}
40+
: config
41+
42+
const instance = useMemo(() => createSanityInstance(mainConfig), [mainConfig])
43+
44+
// Ref to hold the scheduled disposal timer.
45+
const disposal = useRef<{
46+
instance: SanityInstance
47+
timeoutId: ReturnType<typeof setTimeout>
48+
} | null>(null)
49+
50+
useEffect(() => {
51+
// If the component remounts quickly (as in Strict Mode), cancel any pending disposal.
52+
if (disposal.current !== null && instance === disposal.current.instance) {
53+
clearTimeout(disposal.current.timeoutId)
54+
disposal.current = null
55+
}
56+
57+
return () => {
58+
disposal.current = {
59+
instance,
60+
timeoutId: setTimeout(() => {
61+
if (!instance.isDisposed()) {
62+
instance.dispose()
63+
}
64+
}, 0),
65+
}
66+
}
67+
}, [instance])
68+
69+
let result = (
70+
<SanityInstanceContext.Provider value={instance}>
71+
<Suspense fallback={fallback}>
72+
<AuthBoundary {...props}>{children}</AuthBoundary>
73+
</Suspense>
74+
</SanityInstanceContext.Provider>
75+
)
76+
77+
if (perspective) {
78+
result = <PerspectiveContext.Provider value={perspective}>{result}</PerspectiveContext.Provider>
79+
}
80+
81+
if (projectId || dataset) {
82+
if (!(projectId && dataset)) {
83+
throw new Error('SDKProvider requires either both of projectId/dataset or none.')
4284
}
4385

44-
return (
45-
<ResourceProvider {...configs[index]} fallback={fallback}>
46-
{createNestedProviders(index + 1)}
47-
</ResourceProvider>
86+
result = (
87+
<DefaultSourceContext.Provider value={sourceFor({projectId, dataset})}>
88+
{result}
89+
</DefaultSourceContext.Provider>
4890
)
4991
}
5092

51-
return createNestedProviders(0)
93+
return result
5294
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {type DocumentSource} from '@sanity/sdk'
2+
import {createContext} from 'react'
3+
4+
export const DefaultSourceContext = createContext<DocumentSource | null>(null)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {type PerspectiveHandle} from '@sanity/sdk'
2+
import {createContext} from 'react'
3+
4+
export const PerspectiveContext = createContext<PerspectiveHandle['perspective']>(undefined)

0 commit comments

Comments
 (0)