@sanity/sdk 3.0.0-rc.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_exports/_internal.d.ts +25 -3
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +127 -9
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +1 -1
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/comlink.d.ts +1 -1
- package/dist/_exports/comlink.js +2 -3
- package/dist/_exports/comlink.js.map +1 -1
- package/dist/{authStore-DKFX8fn7.d.ts → authStore-C-OEkGaM.d.ts} +30 -44
- package/dist/authStore-C-OEkGaM.d.ts.map +1 -0
- package/dist/{authStore-CtE9bLHW.js → clientStore-gUALQ0qW.js} +269 -11
- package/dist/{authStore-CtE9bLHW.js.map → clientStore-gUALQ0qW.js.map} +1 -1
- package/dist/{createSanityInstance-DUI1bGIg.d.ts → createSanityInstance-DCePpBYW.d.ts} +2 -2
- package/dist/{createSanityInstance-DUI1bGIg.d.ts.map → createSanityInstance-DCePpBYW.d.ts.map} +1 -1
- package/dist/{getNodeState-CR3__B2D.js → getNodeState-E0hjqFOo.js} +2 -3
- package/dist/{getNodeState-CR3__B2D.js.map → getNodeState-E0hjqFOo.js.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +168 -176
- package/dist/index.js.map +1 -1
- package/dist/{createStateSourceAction-BOKtqFNm.js → object-BwynGPRZ.js} +87 -3
- package/dist/object-BwynGPRZ.js.map +1 -0
- package/dist/{queryStore-DPf9sN3V.js → version-C90rNWaX.js} +48 -16
- package/dist/version-C90rNWaX.js.map +1 -0
- package/package.json +4 -4
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +7 -4
- package/src/access/checkPermissions.test.ts +1 -1
- package/src/access/checkPermissions.ts +1 -1
- package/src/applications/applications.test.ts +4 -4
- package/src/applications/applications.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +2 -2
- package/src/auth/handleAuthCallback.ts +1 -1
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.ts +1 -1
- package/src/auth/studioModeAuth.test.ts +1 -1
- package/src/auth/studioModeAuth.ts +1 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
- package/src/comments/addonDatasetStore.test.ts +2 -2
- package/src/comments/addonDatasetStore.ts +2 -2
- package/src/comments/commentActions.ts +6 -5
- package/src/comments/commentsStore.ts +4 -4
- package/src/datasets/datasets.test.ts +44 -9
- package/src/datasets/datasets.ts +24 -23
- package/src/document/applyDocumentActions.ts +2 -6
- package/src/document/documentConstants.ts +9 -0
- package/src/document/documentStore.concurrency.test.ts +2 -1
- package/src/document/documentStore.test.ts +87 -17
- package/src/document/documentStore.ts +5 -5
- package/src/document/permissions.ts +2 -1
- package/src/document/processActions.test.ts +2 -1
- package/src/document/processMutations.ts +3 -2
- package/src/document/reducers.test.ts +224 -0
- package/src/document/reducers.ts +110 -27
- package/src/favorites/favorites.test.ts +64 -24
- package/src/favorites/favorites.ts +73 -22
- package/src/installations/installations.test.ts +2 -2
- package/src/installations/installations.ts +2 -2
- package/src/organization/organization.test.ts +2 -2
- package/src/organization/organization.ts +1 -1
- package/src/organizations/organizations.test.ts +2 -2
- package/src/organizations/organizations.ts +1 -1
- package/src/presence/presenceStore.test.ts +15 -22
- package/src/presence/presenceStore.ts +3 -2
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +1 -1
- package/src/projection/getProjectionState.test.ts +6 -4
- package/src/projection/getProjectionState.ts +2 -2
- package/src/projects/projects.test.ts +2 -2
- package/src/projects/projects.ts +1 -1
- package/src/query/queryStore.ts +3 -3
- package/src/store/createSanityInstance.test.ts +1 -1
- package/src/store/createSanityInstance.ts +4 -4
- package/src/store/createStoreInstance.test.ts +0 -5
- package/src/store/fetcherStore.test.ts +23 -1
- package/src/store/fetcherStore.ts +3 -3
- package/src/telemetry/telemetryManager.test.ts +1 -1
- package/src/telemetry/telemetryManager.ts +2 -2
- package/src/users/usersStore.test.ts +1 -1
- package/src/users/usersStore.ts +5 -5
- package/src/utils/ids.test.ts +40 -15
- package/src/utils/ids.ts +47 -2
- package/dist/authStore-DKFX8fn7.d.ts.map +0 -1
- package/dist/clientStore-BtE2-q2i.js +0 -90
- package/dist/clientStore-BtE2-q2i.js.map +0 -1
- package/dist/createStateSourceAction-BOKtqFNm.js.map +0 -1
- package/dist/getEnv-D5RGkQLu.js +0 -8
- package/dist/getEnv-D5RGkQLu.js.map +0 -1
- package/dist/logger-DAtJb5RQ.js +0 -173
- package/dist/logger-DAtJb5RQ.js.map +0 -1
- package/dist/object-CxXlSVg7.js +0 -83
- package/dist/object-CxXlSVg7.js.map +0 -1
- package/dist/queryStore-DPf9sN3V.js.map +0 -1
- package/dist/rolldown-runtime-vyAXikos.js +0 -9
- package/dist/telemetryManager-Dq_qCkeI.js +0 -127
- package/dist/telemetryManager-Dq_qCkeI.js.map +0 -1
- package/dist/version-BChjsi4u.js +0 -9
- package/dist/version-BChjsi4u.js.map +0 -1
- package/src/utils/createFetcherStore.test.ts +0 -171
- package/src/utils/createFetcherStore.ts +0 -284
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
SDK_NODE_NAME,
|
|
6
6
|
type StudioResource,
|
|
7
7
|
} from '@sanity/message-protocol'
|
|
8
|
-
import {catchError,
|
|
8
|
+
import {catchError, first, from, map, type Observable, of, switchMap} from 'rxjs'
|
|
9
9
|
|
|
10
10
|
import {getNodeState} from '../comlink/node/getNodeState'
|
|
11
11
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
12
12
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
13
|
-
import {
|
|
13
|
+
import {defineFetcher, defineMutation} from '../store/fetcherStore'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @public
|
|
@@ -22,7 +22,7 @@ export interface FavoriteStatusResponse {
|
|
|
22
22
|
/**
|
|
23
23
|
* @public
|
|
24
24
|
*/
|
|
25
|
-
interface FavoriteDocumentContext extends DocumentHandle {
|
|
25
|
+
export interface FavoriteDocumentContext extends DocumentHandle {
|
|
26
26
|
resourceId: string
|
|
27
27
|
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
28
28
|
schemaName?: string
|
|
@@ -35,12 +35,19 @@ function createFavoriteKey(context: FavoriteDocumentContext): string {
|
|
|
35
35
|
}`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Fetcher for a document's favorite status, read from the dashboard over
|
|
40
|
+
* comlink, on the shared fetcher cache.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export const favorites = defineFetcher<[context: FavoriteDocumentContext], FavoriteStatusResponse>({
|
|
45
|
+
name: 'favorites',
|
|
46
|
+
getKey: (_instance: SanityInstance, context: FavoriteDocumentContext) =>
|
|
47
|
+
createFavoriteKey(context),
|
|
48
|
+
// Tag each entry with its key so `setFavorite` can invalidate exactly it.
|
|
49
|
+
tags: (_data, context) => [{type: 'favorite', id: createFavoriteKey(context)}],
|
|
50
|
+
fetch: (instance) => {
|
|
44
51
|
return (context: FavoriteDocumentContext): Observable<FavoriteStatusResponse> => {
|
|
45
52
|
const nodeStateSource = getNodeState(instance, {
|
|
46
53
|
name: SDK_NODE_NAME,
|
|
@@ -59,8 +66,8 @@ const favorites = createFetcherStore<[FavoriteDocumentContext], FavoriteStatusRe
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
return nodeStateSource.observable.pipe(
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
// Wait until connected, then complete after the single fetch settles.
|
|
70
|
+
first((nodeState) => !!nodeState),
|
|
64
71
|
switchMap((nodeState) => {
|
|
65
72
|
const node = nodeState!.node
|
|
66
73
|
return from(
|
|
@@ -84,19 +91,63 @@ const favorites = createFetcherStore<[FavoriteDocumentContext], FavoriteStatusRe
|
|
|
84
91
|
})
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @param context - The document context including ID, type, and resource information.
|
|
90
|
-
* @returns A StateSource emitting `{ isFavorited: boolean }`.
|
|
94
|
+
* Input for {@link setFavorite}: the document context plus the desired state.
|
|
95
|
+
*
|
|
91
96
|
* @public
|
|
92
97
|
*/
|
|
93
|
-
export
|
|
98
|
+
export type SetFavoriteInput = FavoriteDocumentContext & {
|
|
99
|
+
/** The favorite state to move to: `true` to favorite, `false` to unfavorite. */
|
|
100
|
+
isFavorited: boolean
|
|
101
|
+
}
|
|
94
102
|
|
|
95
103
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* @
|
|
104
|
+
* Sets a document's favorite state over comlink, then invalidates the cached
|
|
105
|
+
* {@link favorites} status for that document so active readers reconverge on
|
|
106
|
+
* server truth. The write-side counterpart to {@link favorites}.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
101
109
|
*/
|
|
102
|
-
export const
|
|
110
|
+
export const setFavorite = defineMutation<SetFavoriteInput, FavoriteStatusResponse>({
|
|
111
|
+
name: 'setFavorite',
|
|
112
|
+
mutationFn: (instance) => {
|
|
113
|
+
return ({isFavorited, ...context}: SetFavoriteInput): Observable<FavoriteStatusResponse> => {
|
|
114
|
+
const nodeStateSource = getNodeState(instance, {
|
|
115
|
+
name: SDK_NODE_NAME,
|
|
116
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
117
|
+
})
|
|
118
|
+
const payload = {
|
|
119
|
+
eventType: isFavorited ? 'added' : 'removed',
|
|
120
|
+
document: {
|
|
121
|
+
id: context.documentId,
|
|
122
|
+
type: context.documentType,
|
|
123
|
+
resource: {
|
|
124
|
+
id: context.resourceId,
|
|
125
|
+
type: context.resourceType,
|
|
126
|
+
...(context.schemaName ? {schemaName: context.schemaName} : {}),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return nodeStateSource.observable.pipe(
|
|
132
|
+
// Wait until connected, then complete after the single mutation settles.
|
|
133
|
+
first((nodeState) => !!nodeState),
|
|
134
|
+
switchMap((nodeState) => {
|
|
135
|
+
const node = nodeState!.node
|
|
136
|
+
return from(
|
|
137
|
+
node.fetch(
|
|
138
|
+
// @ts-expect-error -- getOrCreateNode should be refactored to take type arguments
|
|
139
|
+
'dashboard/v1/events/favorite/mutate',
|
|
140
|
+
payload,
|
|
141
|
+
) as Promise<{success: boolean}>,
|
|
142
|
+
).pipe(
|
|
143
|
+
map((response) => {
|
|
144
|
+
if (!response.success) throw new Error('Failed to update favorite status')
|
|
145
|
+
return {isFavorited}
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
invalidates: (_result, input) => [{type: 'favorite', id: createFavoriteKey(input)}],
|
|
153
|
+
})
|
|
@@ -40,7 +40,7 @@ describe('installations', () => {
|
|
|
40
40
|
scope: 'global',
|
|
41
41
|
})
|
|
42
42
|
expect(request).toHaveBeenCalledWith({
|
|
43
|
-
|
|
43
|
+
url: '/installations',
|
|
44
44
|
query: {organizationId: 'org1', include: 'interfaces'},
|
|
45
45
|
tag: 'installations.list',
|
|
46
46
|
})
|
|
@@ -54,7 +54,7 @@ describe('installations', () => {
|
|
|
54
54
|
|
|
55
55
|
expect(result).toEqual(inst)
|
|
56
56
|
expect(request).toHaveBeenCalledWith({
|
|
57
|
-
|
|
57
|
+
url: '/installations/inst1',
|
|
58
58
|
query: {},
|
|
59
59
|
tag: 'installations.get',
|
|
60
60
|
})
|
|
@@ -160,7 +160,7 @@ export const installations = defineFetcher<
|
|
|
160
160
|
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
161
161
|
switchMap((client) =>
|
|
162
162
|
client.observable.request<InstallationsResponse<InstallationInclude>>({
|
|
163
|
-
|
|
163
|
+
url: '/installations',
|
|
164
164
|
query: buildQuery({
|
|
165
165
|
organizationId: options.organizationId,
|
|
166
166
|
include: serializeInclude(options.include),
|
|
@@ -196,7 +196,7 @@ export const installation = defineFetcher<
|
|
|
196
196
|
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
197
197
|
switchMap((client) =>
|
|
198
198
|
client.observable.request<Installation<InstallationInclude>>({
|
|
199
|
-
|
|
199
|
+
url: `/installations/${installationId}`,
|
|
200
200
|
query: buildQuery({include: serializeInclude(options?.include)}),
|
|
201
201
|
tag: 'installations.get',
|
|
202
202
|
}),
|
|
@@ -35,7 +35,7 @@ describe('organization', () => {
|
|
|
35
35
|
const result = await organization.resolveState(instance, {organizationId: 'org_1'})
|
|
36
36
|
expect(result).toEqual(detail)
|
|
37
37
|
expect(request).toHaveBeenCalledWith({
|
|
38
|
-
|
|
38
|
+
url: '/organizations/org_1',
|
|
39
39
|
query: {includeMembers: 'false', includeFeatures: 'false'},
|
|
40
40
|
tag: 'organizations.get',
|
|
41
41
|
})
|
|
@@ -58,7 +58,7 @@ describe('organization', () => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
expect(request).toHaveBeenCalledWith({
|
|
61
|
-
|
|
61
|
+
url: '/organizations/org_1',
|
|
62
62
|
query: {
|
|
63
63
|
includeMembers: 'true',
|
|
64
64
|
includeFeatures: 'true',
|
|
@@ -128,7 +128,7 @@ export const organization = defineFetcher<
|
|
|
128
128
|
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
129
129
|
switchMap((client) =>
|
|
130
130
|
client.observable.request<Organization<boolean, boolean>>({
|
|
131
|
-
|
|
131
|
+
url: `/organizations/${resolveOrganizationId(options)}`,
|
|
132
132
|
query: buildQuery(normalizeOrganizationOptions(options)),
|
|
133
133
|
tag: 'organizations.get',
|
|
134
134
|
}),
|
|
@@ -35,7 +35,7 @@ describe('organizations', () => {
|
|
|
35
35
|
const result = await organizations.resolveState(instance)
|
|
36
36
|
expect(result).toEqual(list)
|
|
37
37
|
expect(request).toHaveBeenCalledWith({
|
|
38
|
-
|
|
38
|
+
url: '/organizations',
|
|
39
39
|
query: {
|
|
40
40
|
includeImplicitMemberships: 'false',
|
|
41
41
|
includeMembers: 'false',
|
|
@@ -62,7 +62,7 @@ describe('organizations', () => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
expect(request).toHaveBeenCalledWith({
|
|
65
|
-
|
|
65
|
+
url: '/organizations',
|
|
66
66
|
query: {
|
|
67
67
|
includeImplicitMemberships: 'true',
|
|
68
68
|
includeMembers: 'true',
|
|
@@ -94,7 +94,7 @@ export const organizations = defineFetcher<
|
|
|
94
94
|
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
95
95
|
switchMap((client) =>
|
|
96
96
|
client.observable.request<Organizations<boolean, boolean>>({
|
|
97
|
-
|
|
97
|
+
url: '/organizations',
|
|
98
98
|
query: buildQuery(normalizeOrganizationsOptions(options)),
|
|
99
99
|
tag: 'organizations.list',
|
|
100
100
|
}),
|
|
@@ -24,6 +24,12 @@ vi.mock('../client/clientStore')
|
|
|
24
24
|
vi.mock('../users/usersStore')
|
|
25
25
|
vi.mock('./bifurTransport')
|
|
26
26
|
|
|
27
|
+
function lastTransportSessionId(): string {
|
|
28
|
+
const call = vi.mocked(createBifurTransport).mock.calls.at(-1)
|
|
29
|
+
if (!call) throw new Error('createBifurTransport was not called')
|
|
30
|
+
return call[0].sessionId
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
describe('presenceStore', () => {
|
|
28
34
|
let instance: SanityInstance
|
|
29
35
|
let mockClient: SanityClient
|
|
@@ -49,13 +55,6 @@ describe('presenceStore', () => {
|
|
|
49
55
|
beforeEach(() => {
|
|
50
56
|
vi.clearAllMocks()
|
|
51
57
|
|
|
52
|
-
// Mock crypto.randomUUID
|
|
53
|
-
Object.defineProperty(global, 'crypto', {
|
|
54
|
-
value: {
|
|
55
|
-
randomUUID: vi.fn(() => 'test-session-id'),
|
|
56
|
-
},
|
|
57
|
-
})
|
|
58
|
-
|
|
59
58
|
mockClient = {
|
|
60
59
|
withConfig: vi.fn().mockReturnThis(),
|
|
61
60
|
observable: {
|
|
@@ -103,7 +102,7 @@ describe('presenceStore', () => {
|
|
|
103
102
|
expect(createBifurTransport).toHaveBeenCalledWith({
|
|
104
103
|
client: mockClient,
|
|
105
104
|
token$: expect.any(Object),
|
|
106
|
-
sessionId:
|
|
105
|
+
sessionId: expect.stringMatching(/^[a-zA-Z0-9]{16}$/),
|
|
107
106
|
})
|
|
108
107
|
})
|
|
109
108
|
|
|
@@ -164,7 +163,7 @@ describe('presenceStore', () => {
|
|
|
164
163
|
mockIncomingEvents.next({
|
|
165
164
|
type: 'state',
|
|
166
165
|
userId: 'user-1',
|
|
167
|
-
sessionId:
|
|
166
|
+
sessionId: lastTransportSessionId(), // Same as our session
|
|
168
167
|
timestamp: '2023-01-01T12:00:00Z',
|
|
169
168
|
locations: [],
|
|
170
169
|
})
|
|
@@ -314,7 +313,7 @@ describe('presenceStore', () => {
|
|
|
314
313
|
getPresence(instance, {resource: canvasResource})
|
|
315
314
|
|
|
316
315
|
expect(mockClient.observable.request).toHaveBeenCalledWith({
|
|
317
|
-
|
|
316
|
+
url: '/canvases/canvas123',
|
|
318
317
|
tag: 'canvases.get',
|
|
319
318
|
})
|
|
320
319
|
})
|
|
@@ -1050,7 +1049,7 @@ describe('presenceStore', () => {
|
|
|
1050
1049
|
mockIncomingEvents.next({
|
|
1051
1050
|
type: 'rollCall',
|
|
1052
1051
|
userId: 'user-1',
|
|
1053
|
-
sessionId:
|
|
1052
|
+
sessionId: lastTransportSessionId(),
|
|
1054
1053
|
})
|
|
1055
1054
|
await flush()
|
|
1056
1055
|
|
|
@@ -1139,25 +1138,19 @@ describe('presenceStore', () => {
|
|
|
1139
1138
|
})
|
|
1140
1139
|
|
|
1141
1140
|
describe('session id', () => {
|
|
1142
|
-
afterEach(() => {
|
|
1143
|
-
vi.unstubAllGlobals()
|
|
1144
|
-
})
|
|
1145
|
-
|
|
1146
1141
|
it('gives each store its own id, so nothing can collide across tabs', () => {
|
|
1147
1142
|
// Ids are deliberately not persisted: a tab that inherits another's
|
|
1148
1143
|
// session storage would otherwise reuse the id and the two live clients
|
|
1149
1144
|
// would filter each other out as their own session.
|
|
1150
|
-
let counter = 0
|
|
1151
|
-
vi.stubGlobal('crypto', {randomUUID: vi.fn(() => `session-${++counter}`)})
|
|
1152
|
-
|
|
1153
1145
|
getPresence(instance, {resource: {projectId: 'p1', dataset: 'd1'}}).subscribe(() => {})
|
|
1154
|
-
const first =
|
|
1146
|
+
const first = lastTransportSessionId()
|
|
1155
1147
|
|
|
1156
1148
|
getPresence(instance, {resource: {projectId: 'p2', dataset: 'd2'}}).subscribe(() => {})
|
|
1157
|
-
const second =
|
|
1149
|
+
const second = lastTransportSessionId()
|
|
1158
1150
|
|
|
1159
|
-
expect(first).
|
|
1160
|
-
expect(second).
|
|
1151
|
+
expect(first).toMatch(/^[a-zA-Z0-9]{16}$/)
|
|
1152
|
+
expect(second).toMatch(/^[a-zA-Z0-9]{16}$/)
|
|
1153
|
+
expect(first).not.toBe(second)
|
|
1161
1154
|
})
|
|
1162
1155
|
})
|
|
1163
1156
|
})
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
39
39
|
import {type SanityUser} from '../users/types'
|
|
40
40
|
import {getUserState} from '../users/usersStore'
|
|
41
|
+
import {randomId} from '../utils/ids'
|
|
41
42
|
import {createLogger} from '../utils/logger'
|
|
42
43
|
import {createBifurTransport} from './bifurTransport'
|
|
43
44
|
import {startsWithPath} from './paths'
|
|
@@ -198,7 +199,7 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
|
|
|
198
199
|
// storage (`window.open`, or a same-origin iframe), making two live clients
|
|
199
200
|
// filter each other out as self. Stale sessions are handled by the
|
|
200
201
|
// disconnect on unload, with the expiry sweep above as the backstop.
|
|
201
|
-
const sessionId =
|
|
202
|
+
const sessionId = randomId(16)
|
|
202
203
|
|
|
203
204
|
// Dataset resources must use the project hostname so the socket URL is project-specific.
|
|
204
205
|
// Canvas resources use the global API endpoint via the resource config.
|
|
@@ -411,7 +412,7 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
|
|
|
411
412
|
subscription.add(
|
|
412
413
|
globalClient.observable
|
|
413
414
|
.request<{organizationId: string}>({
|
|
414
|
-
|
|
415
|
+
url: `/canvases/${resource.canvasId}`,
|
|
415
416
|
tag: 'canvases.get',
|
|
416
417
|
})
|
|
417
418
|
.subscribe({
|
|
@@ -36,7 +36,7 @@ describe('project', () => {
|
|
|
36
36
|
const result = await project.resolveState(instance, {projectId: 'a'})
|
|
37
37
|
expect(result).toEqual(detail)
|
|
38
38
|
expect(request).toHaveBeenCalledWith({
|
|
39
|
-
|
|
39
|
+
url: '/projects/a',
|
|
40
40
|
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
41
41
|
tag: 'projects.get',
|
|
42
42
|
})
|
|
@@ -59,7 +59,7 @@ describe('project', () => {
|
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
expect(request).toHaveBeenCalledWith({
|
|
62
|
-
|
|
62
|
+
url: '/projects/a',
|
|
63
63
|
query: {
|
|
64
64
|
includeMembers: 'false',
|
|
65
65
|
includeFeatures: 'false',
|
|
@@ -80,7 +80,7 @@ describe('project', () => {
|
|
|
80
80
|
|
|
81
81
|
await project.resolveState(instance)
|
|
82
82
|
expect(request).toHaveBeenCalledWith({
|
|
83
|
-
|
|
83
|
+
url: '/projects/p',
|
|
84
84
|
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
85
85
|
tag: 'projects.get',
|
|
86
86
|
})
|
package/src/project/project.ts
CHANGED
|
@@ -130,7 +130,7 @@ export const project = defineFetcher<
|
|
|
130
130
|
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
131
131
|
switchMap((client) =>
|
|
132
132
|
client.observable.request<Project<boolean, boolean>>({
|
|
133
|
-
|
|
133
|
+
url: `/projects/${resolveProjectId(instance, options)}`,
|
|
134
134
|
query: buildQuery(normalizeProjectOptions(options)),
|
|
135
135
|
tag: 'projects.get',
|
|
136
136
|
}),
|
|
@@ -4,7 +4,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
4
4
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
5
5
|
import {type StoreState} from '../store/createStoreState'
|
|
6
6
|
import {hashString} from '../utils/hashString'
|
|
7
|
-
import {
|
|
7
|
+
import {randomId} from '../utils/ids'
|
|
8
8
|
import {getProjectionState} from './getProjectionState'
|
|
9
9
|
import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
|
|
10
10
|
import {type ProjectionStoreState} from './types'
|
|
@@ -19,7 +19,7 @@ vi.mock('../utils/ids', async (importOriginal) => {
|
|
|
19
19
|
// Mock implementation uses the external counter
|
|
20
20
|
return {
|
|
21
21
|
...util,
|
|
22
|
-
|
|
22
|
+
randomId: vi.fn(() => {
|
|
23
23
|
const id = `testSubId_${++mockIdCounter}`
|
|
24
24
|
return id
|
|
25
25
|
}),
|
|
@@ -39,8 +39,7 @@ describe('getProjectionState', () => {
|
|
|
39
39
|
let state: StoreState<ProjectionStoreState & {extra?: unknown}>
|
|
40
40
|
|
|
41
41
|
beforeEach(() => {
|
|
42
|
-
|
|
43
|
-
vi.mocked(insecureRandomId).mockClear()
|
|
42
|
+
vi.mocked(randomId).mockClear()
|
|
44
43
|
|
|
45
44
|
// Capture state
|
|
46
45
|
vi.mocked(subscribeToStateAndFetchBatches).mockImplementation((context) => {
|
|
@@ -49,6 +48,9 @@ describe('getProjectionState', () => {
|
|
|
49
48
|
})
|
|
50
49
|
|
|
51
50
|
instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
|
|
51
|
+
// Reset after instance creation: the instanceId also comes from randomId,
|
|
52
|
+
// but the test ids should only count subscription-related calls
|
|
53
|
+
mockIdCounter = 0
|
|
52
54
|
vi.useFakeTimers() // Enable fake timers for each test
|
|
53
55
|
})
|
|
54
56
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type StateSource,
|
|
11
11
|
} from '../store/createStateSourceAction'
|
|
12
12
|
import {hashString} from '../utils/hashString'
|
|
13
|
-
import {
|
|
13
|
+
import {randomId} from '../utils/ids'
|
|
14
14
|
import {omitProperty} from '../utils/object'
|
|
15
15
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
16
16
|
import {projectionStore} from './projectionStore'
|
|
@@ -85,7 +85,7 @@ export const _getProjectionState = bindActionByResourceAndPerspective(
|
|
|
85
85
|
},
|
|
86
86
|
onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
|
|
87
87
|
const {projection, ...docHandle} = options
|
|
88
|
-
const subscriptionId =
|
|
88
|
+
const subscriptionId = randomId(16)
|
|
89
89
|
const documentId = getPublishedId(DocumentId(docHandle.documentId))
|
|
90
90
|
const validProjection = validateProjection(projection)
|
|
91
91
|
const projectionHash = hashString(validProjection)
|
|
@@ -35,7 +35,7 @@ describe('projects', () => {
|
|
|
35
35
|
const result = await projects.resolveState(instance)
|
|
36
36
|
expect(result).toEqual(list)
|
|
37
37
|
expect(request).toHaveBeenCalledWith({
|
|
38
|
-
|
|
38
|
+
url: '/projects',
|
|
39
39
|
query: {includeMembers: 'false', includeFeatures: 'true', onlyExplicitMembership: 'false'},
|
|
40
40
|
tag: 'projects.list',
|
|
41
41
|
})
|
|
@@ -58,7 +58,7 @@ describe('projects', () => {
|
|
|
58
58
|
})
|
|
59
59
|
|
|
60
60
|
expect(request).toHaveBeenCalledWith({
|
|
61
|
-
|
|
61
|
+
url: '/projects',
|
|
62
62
|
query: {
|
|
63
63
|
organizationId: 'org123',
|
|
64
64
|
includeMembers: 'true',
|
package/src/projects/projects.ts
CHANGED
|
@@ -58,7 +58,7 @@ export const projects = defineFetcher<
|
|
|
58
58
|
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
59
59
|
switchMap((client) =>
|
|
60
60
|
client.observable.request<Project<boolean, boolean>[]>({
|
|
61
|
-
|
|
61
|
+
url: '/projects',
|
|
62
62
|
query: buildQuery(normalizeProjectsOptions(options)),
|
|
63
63
|
tag: 'projects.list',
|
|
64
64
|
}),
|
package/src/query/queryStore.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from '../store/createStateSourceAction'
|
|
37
37
|
import {type StoreState} from '../store/createStoreState'
|
|
38
38
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
39
|
-
import {
|
|
39
|
+
import {randomId} from '../utils/ids'
|
|
40
40
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
41
41
|
import {
|
|
42
42
|
QUERY_STATE_CLEAR_DELAY,
|
|
@@ -309,7 +309,7 @@ const _getQueryState = bindActionByResource(
|
|
|
309
309
|
return queryState?.result
|
|
310
310
|
},
|
|
311
311
|
onSubscribe: ({state, instance}, options: QueryOptions) => {
|
|
312
|
-
const subscriptionId =
|
|
312
|
+
const subscriptionId = randomId(16)
|
|
313
313
|
const key = getQueryKey(instance, options)
|
|
314
314
|
|
|
315
315
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
@@ -370,7 +370,7 @@ const _resolveQuery = bindActionByResource(
|
|
|
370
370
|
// while a component is suspended leaves a subscriber-less key behind — the
|
|
371
371
|
// component never commits, so no other subscriber exists — and its stored
|
|
372
372
|
// error would be rethrown on every future mount without ever refetching.
|
|
373
|
-
const subscriptionId =
|
|
373
|
+
const subscriptionId = randomId(16)
|
|
374
374
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
375
375
|
|
|
376
376
|
const aborted$ = signal
|
|
@@ -6,7 +6,7 @@ import {createSanityInstance} from './createSanityInstance'
|
|
|
6
6
|
describe('createSanityInstance', () => {
|
|
7
7
|
it('should create an instance with a unique instanceId and given config', () => {
|
|
8
8
|
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
9
|
-
expect(
|
|
9
|
+
expect(instance.instanceId).toMatch(/^[a-zA-Z0-9]{16}$/)
|
|
10
10
|
expect(instance.config).toEqual({projectId: 'proj1', dataset: 'ds1'})
|
|
11
11
|
expect(instance.isDisposed()).toBe(false)
|
|
12
12
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {type SanityConfig} from '../config/sanityConfig'
|
|
2
|
-
import {
|
|
2
|
+
import {randomId} from '../utils/ids'
|
|
3
3
|
import {createLogger, type InstanceContext} from '../utils/logger'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,7 +10,7 @@ import {createLogger, type InstanceContext} from '../utils/logger'
|
|
|
10
10
|
export interface SanityInstance {
|
|
11
11
|
/**
|
|
12
12
|
* Unique identifier for this instance
|
|
13
|
-
* @remarks Generated
|
|
13
|
+
* @remarks Generated as a random 16-character alphanumeric ID
|
|
14
14
|
*/
|
|
15
15
|
readonly instanceId: string
|
|
16
16
|
|
|
@@ -47,7 +47,7 @@ export interface SanityInstance {
|
|
|
47
47
|
* @public
|
|
48
48
|
*/
|
|
49
49
|
export function createSanityInstance(config: SanityConfig = {}): SanityInstance {
|
|
50
|
-
const instanceId =
|
|
50
|
+
const instanceId = randomId(16)
|
|
51
51
|
const disposeListeners = new Map<string, () => void>()
|
|
52
52
|
const disposed = {current: false}
|
|
53
53
|
|
|
@@ -99,7 +99,7 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
|
|
|
99
99
|
logger.info('Instance disposed')
|
|
100
100
|
},
|
|
101
101
|
onDispose: (cb) => {
|
|
102
|
-
const listenerId =
|
|
102
|
+
const listenerId = randomId(16)
|
|
103
103
|
disposeListeners.set(listenerId, cb)
|
|
104
104
|
return () => {
|
|
105
105
|
disposeListeners.delete(listenerId)
|
|
@@ -8,11 +8,6 @@ describe('createStoreInstance', () => {
|
|
|
8
8
|
let instance: ReturnType<typeof createSanityInstance>
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
11
|
-
// Mock crypto for predictable instance IDs
|
|
12
|
-
vi.stubGlobal('crypto', {
|
|
13
|
-
randomUUID: () => 'test-uuid-1234',
|
|
14
|
-
})
|
|
15
|
-
|
|
16
11
|
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
17
12
|
})
|
|
18
13
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {NEVER, of, startWith, Subject, throwError} from 'rxjs'
|
|
1
|
+
import {concatWith, NEVER, of, startWith, Subject, throwError} from 'rxjs'
|
|
2
2
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
4
|
import {createSanityInstance, type SanityInstance} from './createSanityInstance'
|
|
@@ -65,6 +65,28 @@ describe('fetcherStore', () => {
|
|
|
65
65
|
unsub3()
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
+
it('revalidates a stale entry on re-subscribe when the fetch source does not complete', async () => {
|
|
69
|
+
let n = 0
|
|
70
|
+
const fetchSpy = vi.fn(() => of(++n).pipe(concatWith(NEVER)))
|
|
71
|
+
const fetcher = defineFetcher<[], number>({
|
|
72
|
+
name: 'swr-non-completing',
|
|
73
|
+
staleTime: 0,
|
|
74
|
+
getKey: () => 'k',
|
|
75
|
+
fetch: () => fetchSpy,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const first = fetcher.getState(instance)
|
|
79
|
+
const unsub1 = first.subscribe()
|
|
80
|
+
await vi.waitFor(() => expect(first.getCurrent().data).toBe(1))
|
|
81
|
+
unsub1()
|
|
82
|
+
|
|
83
|
+
const second = fetcher.getState(instance)
|
|
84
|
+
const unsub2 = second.subscribe()
|
|
85
|
+
await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(2))
|
|
86
|
+
expect(second.getCurrent().data).toBe(2)
|
|
87
|
+
unsub2()
|
|
88
|
+
})
|
|
89
|
+
|
|
68
90
|
it('invalidate refetches an actively-subscribed entry', async () => {
|
|
69
91
|
let n = 0
|
|
70
92
|
const fetcher = defineFetcher<[], number>({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {filter, firstValueFrom, map, type Observable, Subscription} from 'rxjs'
|
|
1
|
+
import {filter, firstValueFrom, map, type Observable, Subscription, take} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {setCleanupInterval, setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
4
4
|
import {bindActionGlobally} from './createActionBinder'
|
|
@@ -490,6 +490,7 @@ const startFetch = (
|
|
|
490
490
|
sub.add(
|
|
491
491
|
def
|
|
492
492
|
.fetch(instance)(...params)
|
|
493
|
+
.pipe(take(1))
|
|
493
494
|
.subscribe({
|
|
494
495
|
next: (data) => {
|
|
495
496
|
const current = state.get().entries[key]
|
|
@@ -659,8 +660,7 @@ export function defineFetcher<TParams extends unknown[], TData>(
|
|
|
659
660
|
}))
|
|
660
661
|
startInterval(state, def, instance, params, key)
|
|
661
662
|
|
|
662
|
-
// Defer the fetch decision off the subscribe call stack
|
|
663
|
-
// `delay(0)` precedent in the legacy fetcher store) to avoid a
|
|
663
|
+
// Defer the fetch decision off the subscribe call stack to avoid a
|
|
664
664
|
// re-entrant cache write during subscription setup.
|
|
665
665
|
queueMicrotask(() => {
|
|
666
666
|
const entry = state.get().entries[key]
|
|
@@ -280,7 +280,7 @@ describe('createTelemetryManager', () => {
|
|
|
280
280
|
const result = await manager.checkConsent()
|
|
281
281
|
expect(result).toBe(true)
|
|
282
282
|
expect(mockClient.request).toHaveBeenCalledWith(
|
|
283
|
-
expect.objectContaining({
|
|
283
|
+
expect.objectContaining({url: '/intake/telemetry-status'}),
|
|
284
284
|
)
|
|
285
285
|
})
|
|
286
286
|
|
|
@@ -86,7 +86,7 @@ export function createTelemetryManager(options: TelemetryManagerOptions): Teleme
|
|
|
86
86
|
try {
|
|
87
87
|
const client = getClient()
|
|
88
88
|
const result = await client.request<{status: ConsentStatus}>({
|
|
89
|
-
|
|
89
|
+
url: '/intake/telemetry-status',
|
|
90
90
|
tag: CONSENT_TAG,
|
|
91
91
|
})
|
|
92
92
|
cachedConsent = result
|
|
@@ -120,7 +120,7 @@ export function createTelemetryManager(options: TelemetryManagerOptions): Teleme
|
|
|
120
120
|
const client = getClient()
|
|
121
121
|
log.debug('sending event batch', {batchSize: batch.length, environment})
|
|
122
122
|
return client.request({
|
|
123
|
-
|
|
123
|
+
url: '/intake/batch',
|
|
124
124
|
method: 'POST',
|
|
125
125
|
body: {projectId, batch: enrichBatch(batch)},
|
|
126
126
|
tag: BATCH_TAG,
|