@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.2
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/README.md +12 -0
- package/dist/_exports/_internal.d.ts +163 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +332 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/_exports/agent.d.ts +65 -0
- package/dist/_exports/agent.d.ts.map +1 -0
- package/dist/_exports/agent.js +71 -0
- package/dist/_exports/agent.js.map +1 -0
- package/dist/_exports/comlink.d.ts +118 -0
- package/dist/_exports/comlink.d.ts.map +1 -0
- package/dist/_exports/comlink.js +69 -0
- package/dist/_exports/comlink.js.map +1 -0
- package/dist/authStore-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/authStore-DKFX8fn7.d.ts +3179 -0
- package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
- package/dist/clientStore-BtE2-q2i.js +90 -0
- package/dist/clientStore-BtE2-q2i.js.map +1 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
- package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
- package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
- package/dist/getEnv-D5RGkQLu.js +8 -0
- package/dist/getEnv-D5RGkQLu.js.map +1 -0
- package/dist/getNodeState-CR3__B2D.js +67 -0
- package/dist/getNodeState-CR3__B2D.js.map +1 -0
- package/dist/index.d.ts +3 -2332
- package/dist/index.js +4171 -4332
- package/dist/index.js.map +1 -1
- package/dist/logger-DAtJb5RQ.js +173 -0
- package/dist/logger-DAtJb5RQ.js.map +1 -0
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/queryStore-DPf9sN3V.js +1346 -0
- package/dist/queryStore-DPf9sN3V.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
- package/dist/telemetryManager-Dq_qCkeI.js +127 -0
- package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
- package/dist/version-BChjsi4u.js +9 -0
- package/dist/version-BChjsi4u.js.map +1 -0
- package/package.json +47 -52
- package/src/_exports/_internal.ts +27 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +125 -64
- 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 +88 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +367 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +58 -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/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +40 -82
- 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 +35 -13
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +15 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +753 -273
- package/src/document/documentStore.ts +197 -75
- 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 +76 -2
- package/src/document/patchOperations.ts +60 -1
- package/src/document/permissions.test.ts +107 -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 +772 -64
- package/src/document/reducers.ts +205 -51
- 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.test.ts +46 -0
- package/src/document/util.ts +50 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +73 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +206 -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 +201 -9
- package/src/presence/bifurTransport.ts +98 -14
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +915 -24
- package/src/presence/presenceStore.ts +576 -104
- package/src/presence/types.ts +175 -3
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/types.ts +0 -15
- 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 -9
- 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/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 +85 -69
- 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 +109 -90
- package/src/store/createActionBinder.ts +36 -28
- package/src/store/createSanityInstance.test.ts +15 -13
- package/src/store/createSanityInstance.ts +14 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +38 -43
- 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 +3 -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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {of} from 'rxjs'
|
|
3
|
+
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
+
import {getOrganizationsCacheKey, organizations} from './organizations'
|
|
9
|
+
|
|
10
|
+
vi.mock('../client/clientStore')
|
|
11
|
+
|
|
12
|
+
describe('organizations', () => {
|
|
13
|
+
let instance: SanityInstance
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
instance.dispose()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('calls `client.observable.request` against `/organizations` and returns the result', async () => {
|
|
24
|
+
const list = [{id: 'org_1'}, {id: 'org_2'}]
|
|
25
|
+
const request = vi.fn().mockReturnValue(of(list))
|
|
26
|
+
|
|
27
|
+
const mockClient = {
|
|
28
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
29
|
+
} as SanityClient
|
|
30
|
+
|
|
31
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
32
|
+
observable: of(mockClient),
|
|
33
|
+
} as StateSource<SanityClient>)
|
|
34
|
+
|
|
35
|
+
const result = await organizations.resolveState(instance)
|
|
36
|
+
expect(result).toEqual(list)
|
|
37
|
+
expect(request).toHaveBeenCalledWith({
|
|
38
|
+
uri: '/organizations',
|
|
39
|
+
query: {
|
|
40
|
+
includeImplicitMemberships: 'false',
|
|
41
|
+
includeMembers: 'false',
|
|
42
|
+
includeFeatures: 'false',
|
|
43
|
+
},
|
|
44
|
+
tag: 'organizations.list',
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('serializes query params (booleans → strings) and respects flags', async () => {
|
|
49
|
+
const request = vi.fn().mockReturnValue(of([]))
|
|
50
|
+
const mockClient = {
|
|
51
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
52
|
+
} as SanityClient
|
|
53
|
+
|
|
54
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
55
|
+
observable: of(mockClient),
|
|
56
|
+
} as StateSource<SanityClient>)
|
|
57
|
+
|
|
58
|
+
await organizations.resolveState(instance, {
|
|
59
|
+
includeMembers: true,
|
|
60
|
+
includeFeatures: true,
|
|
61
|
+
includeImplicitMemberships: true,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(request).toHaveBeenCalledWith({
|
|
65
|
+
uri: '/organizations',
|
|
66
|
+
query: {
|
|
67
|
+
includeImplicitMemberships: 'true',
|
|
68
|
+
includeMembers: 'true',
|
|
69
|
+
includeFeatures: 'true',
|
|
70
|
+
},
|
|
71
|
+
tag: 'organizations.list',
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('organizations cache key generation', () => {
|
|
77
|
+
let instance: SanityInstance
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
instance = createSanityInstance({})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
instance.dispose()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('default call excludes all segments (all flags default-false)', () => {
|
|
88
|
+
expect(getOrganizationsCacheKey(instance)).toBe('organizations')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
92
|
+
expect(getOrganizationsCacheKey(instance)).toBe(
|
|
93
|
+
getOrganizationsCacheKey(instance, {
|
|
94
|
+
includeMembers: false,
|
|
95
|
+
includeFeatures: false,
|
|
96
|
+
includeImplicitMemberships: false,
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('explicit includeMembers: true appends :members', () => {
|
|
102
|
+
expect(getOrganizationsCacheKey(instance, {includeMembers: true})).toBe('organizations:members')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('explicit includeFeatures: true appends :features', () => {
|
|
106
|
+
expect(getOrganizationsCacheKey(instance, {includeFeatures: true})).toBe(
|
|
107
|
+
'organizations:features',
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('explicit includeImplicitMemberships: true appends :implicit', () => {
|
|
112
|
+
expect(getOrganizationsCacheKey(instance, {includeImplicitMemberships: true})).toBe(
|
|
113
|
+
'organizations:implicit',
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('combines all segments in order', () => {
|
|
118
|
+
expect(
|
|
119
|
+
getOrganizationsCacheKey(instance, {
|
|
120
|
+
includeMembers: true,
|
|
121
|
+
includeFeatures: true,
|
|
122
|
+
includeImplicitMemberships: true,
|
|
123
|
+
}),
|
|
124
|
+
).toBe('organizations:members:features:implicit')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
128
|
+
const keys = new Set([
|
|
129
|
+
getOrganizationsCacheKey(instance),
|
|
130
|
+
getOrganizationsCacheKey(instance, {includeMembers: true}),
|
|
131
|
+
getOrganizationsCacheKey(instance, {includeFeatures: true}),
|
|
132
|
+
getOrganizationsCacheKey(instance, {includeImplicitMemberships: true}),
|
|
133
|
+
getOrganizationsCacheKey(instance, {includeMembers: true, includeFeatures: true}),
|
|
134
|
+
getOrganizationsCacheKey(instance, {
|
|
135
|
+
includeMembers: true,
|
|
136
|
+
includeImplicitMemberships: true,
|
|
137
|
+
}),
|
|
138
|
+
getOrganizationsCacheKey(instance, {
|
|
139
|
+
includeFeatures: true,
|
|
140
|
+
includeImplicitMemberships: true,
|
|
141
|
+
}),
|
|
142
|
+
getOrganizationsCacheKey(instance, {
|
|
143
|
+
includeMembers: true,
|
|
144
|
+
includeFeatures: true,
|
|
145
|
+
includeImplicitMemberships: true,
|
|
146
|
+
}),
|
|
147
|
+
])
|
|
148
|
+
expect(keys.size).toBe(8)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {switchMap} from 'rxjs'
|
|
2
|
+
|
|
3
|
+
import {getClientState} from '../client/clientStore'
|
|
4
|
+
import {
|
|
5
|
+
type OrganizationBase,
|
|
6
|
+
type OrganizationMember,
|
|
7
|
+
type OrganizationOptions,
|
|
8
|
+
} from '../organization/organization'
|
|
9
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
10
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
11
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
12
|
+
|
|
13
|
+
const API_VERSION = 'v2025-02-19'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The list shape returned from `/organizations`, with `members` and/or
|
|
17
|
+
* `features` conditionally included based on the query options used.
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export type Organizations<
|
|
21
|
+
IncludeMembers extends boolean = false,
|
|
22
|
+
IncludeFeatures extends boolean = false,
|
|
23
|
+
> = (Pick<
|
|
24
|
+
OrganizationBase,
|
|
25
|
+
| 'id'
|
|
26
|
+
| 'name'
|
|
27
|
+
| 'slug'
|
|
28
|
+
| 'createdAt'
|
|
29
|
+
| 'updatedAt'
|
|
30
|
+
| 'defaultRoleName'
|
|
31
|
+
| 'dashboardStatus'
|
|
32
|
+
| 'aiFeaturesStatus'
|
|
33
|
+
> &
|
|
34
|
+
// `boolean extends T` is non-distributive — true only when T is the wide
|
|
35
|
+
// `boolean`, in which case the field is optional. Literal `true`/`false`
|
|
36
|
+
// fall through to the strict branch.
|
|
37
|
+
(boolean extends IncludeMembers
|
|
38
|
+
? {members?: OrganizationMember[]}
|
|
39
|
+
: IncludeMembers extends true
|
|
40
|
+
? {members: OrganizationMember[]}
|
|
41
|
+
: unknown) &
|
|
42
|
+
(boolean extends IncludeFeatures
|
|
43
|
+
? {features?: string[]}
|
|
44
|
+
: IncludeFeatures extends true
|
|
45
|
+
? {features: string[]}
|
|
46
|
+
: unknown))[]
|
|
47
|
+
|
|
48
|
+
/** @public */
|
|
49
|
+
export interface OrganizationsOptions<
|
|
50
|
+
IncludeMembers extends boolean = false,
|
|
51
|
+
IncludeFeatures extends boolean = false,
|
|
52
|
+
> extends Omit<OrganizationOptions<IncludeMembers, IncludeFeatures>, 'organizationId'> {
|
|
53
|
+
/**
|
|
54
|
+
* When `true`, includes organisations the user has access to via
|
|
55
|
+
* project-level grants, not just direct organisation memberships.
|
|
56
|
+
*/
|
|
57
|
+
includeImplicitMemberships?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeOrganizationsOptions(options?: OrganizationsOptions<boolean, boolean>) {
|
|
61
|
+
return {
|
|
62
|
+
includeImplicitMemberships: options?.includeImplicitMemberships ?? false,
|
|
63
|
+
includeMembers: options?.includeMembers ?? false,
|
|
64
|
+
includeFeatures: options?.includeFeatures ?? false,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @internal */
|
|
69
|
+
export function getOrganizationsCacheKey(
|
|
70
|
+
_instance: SanityInstance,
|
|
71
|
+
options?: OrganizationsOptions<boolean, boolean>,
|
|
72
|
+
): string {
|
|
73
|
+
const {includeMembers, includeFeatures, includeImplicitMemberships} =
|
|
74
|
+
normalizeOrganizationsOptions(options)
|
|
75
|
+
const membersKey = includeMembers ? ':members' : ''
|
|
76
|
+
const featuresKey = includeFeatures ? ':features' : ''
|
|
77
|
+
const implicitKey = includeImplicitMemberships ? ':implicit' : ''
|
|
78
|
+
return `organizations${membersKey}${featuresKey}${implicitKey}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetcher for the current user's organizations (`GET /organizations`), on the
|
|
83
|
+
* shared fetcher cache.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
export const organizations = defineFetcher<
|
|
88
|
+
[options?: OrganizationsOptions<boolean, boolean>],
|
|
89
|
+
Organizations<boolean, boolean>
|
|
90
|
+
>({
|
|
91
|
+
name: 'organizations',
|
|
92
|
+
getKey: getOrganizationsCacheKey,
|
|
93
|
+
fetch: (instance) => (options) =>
|
|
94
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
95
|
+
switchMap((client) =>
|
|
96
|
+
client.observable.request<Organizations<boolean, boolean>>({
|
|
97
|
+
uri: '/organizations',
|
|
98
|
+
query: buildQuery(normalizeOrganizationsOptions(options)),
|
|
99
|
+
tag: 'organizations.list',
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
tags: (data) => [
|
|
104
|
+
{type: 'organization', id: 'LIST'},
|
|
105
|
+
...data.map((org) => ({type: 'organization', id: org.id})),
|
|
106
|
+
],
|
|
107
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {fromUrl} from '@sanity/bifur-client'
|
|
2
2
|
import {type SanityClient} from '@sanity/client'
|
|
3
|
-
import {of, Subject} from 'rxjs'
|
|
4
|
-
import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
3
|
+
import {type Observable, of, Subject} from 'rxjs'
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
5
5
|
|
|
6
6
|
import {createBifurTransport} from './bifurTransport'
|
|
7
7
|
import {type PresenceLocation, type TransportEvent} from './types'
|
|
@@ -39,22 +39,28 @@ describe('createBifurTransport', () => {
|
|
|
39
39
|
let mockBifurClient: {
|
|
40
40
|
listen: Mock
|
|
41
41
|
request: Mock
|
|
42
|
+
heartbeats: Observable<Date>
|
|
42
43
|
}
|
|
43
44
|
let mockSanityClient: SanityClient
|
|
44
45
|
let token$: Subject<string | null>
|
|
46
|
+
let heartbeats$: Subject<Date>
|
|
45
47
|
|
|
46
48
|
beforeEach(() => {
|
|
47
49
|
vi.useFakeTimers()
|
|
50
|
+
vi.clearAllMocks()
|
|
51
|
+
heartbeats$ = new Subject<Date>()
|
|
48
52
|
mockBifurClient = {
|
|
49
53
|
listen: vi.fn(() => new Subject<never>()),
|
|
50
54
|
request: vi.fn(() => of(undefined)),
|
|
55
|
+
heartbeats: heartbeats$.asObservable(),
|
|
51
56
|
}
|
|
52
57
|
fromUrlMock.mockReturnValue(mockBifurClient)
|
|
53
58
|
|
|
59
|
+
// Default mock is a dataset client using project hostname
|
|
54
60
|
mockSanityClient = {
|
|
55
61
|
config: () => ({
|
|
56
62
|
dataset: 'test-dataset',
|
|
57
|
-
url: '
|
|
63
|
+
url: 'https://test-project.api.sanity.io/v2022-06-30',
|
|
58
64
|
requestTagPrefix: 'test-tag',
|
|
59
65
|
}),
|
|
60
66
|
withConfig: vi.fn().mockReturnThis(),
|
|
@@ -63,7 +69,7 @@ describe('createBifurTransport', () => {
|
|
|
63
69
|
token$ = new Subject<string | null>()
|
|
64
70
|
})
|
|
65
71
|
|
|
66
|
-
it('constructs the bifur client
|
|
72
|
+
it('constructs the bifur client URL for a dataset resource', () => {
|
|
67
73
|
createBifurTransport({
|
|
68
74
|
client: mockSanityClient,
|
|
69
75
|
token$,
|
|
@@ -71,13 +77,51 @@ describe('createBifurTransport', () => {
|
|
|
71
77
|
})
|
|
72
78
|
|
|
73
79
|
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
74
|
-
'
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
'wss://test-project.api.sanity.io/v2022-06-30/socket/test-dataset?tag=test-tag',
|
|
81
|
+
{token$},
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('constructs the bifur client URL for a canvas resource', () => {
|
|
86
|
+
const canvasClient = {
|
|
87
|
+
config: () => ({
|
|
88
|
+
resource: {type: 'canvas', id: 'canvas-123'},
|
|
89
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
90
|
+
requestTagPrefix: 'test-tag',
|
|
91
|
+
}),
|
|
92
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
93
|
+
} as unknown as SanityClient
|
|
94
|
+
|
|
95
|
+
createBifurTransport({
|
|
96
|
+
client: canvasClient,
|
|
97
|
+
token$,
|
|
98
|
+
sessionId: 'session-id-123',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
102
|
+
'wss://api.sanity.io/v2022-06-30/socket/canvases/canvas-123?tag=test-tag',
|
|
103
|
+
{token$},
|
|
78
104
|
)
|
|
79
105
|
})
|
|
80
106
|
|
|
107
|
+
it('throws when no canvas resource or dataset is configured', () => {
|
|
108
|
+
const invalidClient = {
|
|
109
|
+
config: () => ({
|
|
110
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
111
|
+
requestTagPrefix: 'test-tag',
|
|
112
|
+
}),
|
|
113
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
114
|
+
} as unknown as SanityClient
|
|
115
|
+
|
|
116
|
+
expect(() =>
|
|
117
|
+
createBifurTransport({
|
|
118
|
+
client: invalidClient,
|
|
119
|
+
token$,
|
|
120
|
+
sessionId: 'session-id-123',
|
|
121
|
+
}),
|
|
122
|
+
).toThrow('Unable to determine presence URL')
|
|
123
|
+
})
|
|
124
|
+
|
|
81
125
|
it('handles incoming rollCall events', () => {
|
|
82
126
|
const incomingBifurEvents$ = new Subject<IncomingBifurEvent>()
|
|
83
127
|
mockBifurClient.listen.mockReturnValue(incomingBifurEvents$)
|
|
@@ -90,6 +134,8 @@ describe('createBifurTransport', () => {
|
|
|
90
134
|
|
|
91
135
|
const receivedEvents: TransportEvent[] = []
|
|
92
136
|
incomingEvents$.subscribe((event) => receivedEvents.push(event))
|
|
137
|
+
// The listener is established per live connection, so bring one up first.
|
|
138
|
+
heartbeats$.next(new Date())
|
|
93
139
|
|
|
94
140
|
incomingBifurEvents$.next({
|
|
95
141
|
type: 'rollCall',
|
|
@@ -121,6 +167,8 @@ describe('createBifurTransport', () => {
|
|
|
121
167
|
|
|
122
168
|
const receivedEvents: TransportEvent[] = []
|
|
123
169
|
incomingEvents$.subscribe((event) => receivedEvents.push(event))
|
|
170
|
+
// The listener is established per live connection, so bring one up first.
|
|
171
|
+
heartbeats$.next(new Date())
|
|
124
172
|
|
|
125
173
|
const locations: PresenceLocation[] = [
|
|
126
174
|
{type: 'document', documentId: 'doc1', path: ['a'], lastActiveAt: new Date().toISOString()},
|
|
@@ -160,6 +208,8 @@ describe('createBifurTransport', () => {
|
|
|
160
208
|
|
|
161
209
|
const receivedEvents: TransportEvent[] = []
|
|
162
210
|
incomingEvents$.subscribe((event) => receivedEvents.push(event))
|
|
211
|
+
// The listener is established per live connection, so bring one up first.
|
|
212
|
+
heartbeats$.next(new Date())
|
|
163
213
|
|
|
164
214
|
incomingBifurEvents$.next({
|
|
165
215
|
type: 'disconnect',
|
|
@@ -193,6 +243,7 @@ describe('createBifurTransport', () => {
|
|
|
193
243
|
incomingEvents$.subscribe({
|
|
194
244
|
error: (err) => errors.push(err),
|
|
195
245
|
})
|
|
246
|
+
heartbeats$.next(new Date())
|
|
196
247
|
|
|
197
248
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
249
|
incomingBifurEvents$.next({type: 'unknown'} as any)
|
|
@@ -202,7 +253,148 @@ describe('createBifurTransport', () => {
|
|
|
202
253
|
expect(errors[0].message).toContain('Got unknown presence event')
|
|
203
254
|
})
|
|
204
255
|
|
|
205
|
-
describe('
|
|
256
|
+
describe('connections', () => {
|
|
257
|
+
it('emits an incrementing generation each time the socket becomes live', () => {
|
|
258
|
+
const [, , connections$] = createBifurTransport({
|
|
259
|
+
client: mockSanityClient,
|
|
260
|
+
token$,
|
|
261
|
+
sessionId: 'my-session',
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const generations: number[] = []
|
|
265
|
+
connections$.subscribe((generation) => generations.push(generation))
|
|
266
|
+
|
|
267
|
+
// Several heartbeats on one live socket are a single generation.
|
|
268
|
+
heartbeats$.next(new Date())
|
|
269
|
+
heartbeats$.next(new Date())
|
|
270
|
+
expect(generations).toEqual([1])
|
|
271
|
+
|
|
272
|
+
// bifur-client errors the stream when the socket closes. The transport is
|
|
273
|
+
// expected to reconnect rather than give up for the life of the page.
|
|
274
|
+
const reconnected$ = new Subject<Date>()
|
|
275
|
+
mockBifurClient.heartbeats = reconnected$.asObservable()
|
|
276
|
+
heartbeats$.error(new Error('WebSocket connection error'))
|
|
277
|
+
|
|
278
|
+
// First retry is scheduled 200ms out (2 ** 1 * 100).
|
|
279
|
+
vi.advanceTimersByTime(200)
|
|
280
|
+
reconnected$.next(new Date())
|
|
281
|
+
|
|
282
|
+
expect(generations).toEqual([1, 2])
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('does not resubscribe before the backoff has elapsed', () => {
|
|
286
|
+
const [, , connections$] = createBifurTransport({
|
|
287
|
+
client: mockSanityClient,
|
|
288
|
+
token$,
|
|
289
|
+
sessionId: 'my-session',
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const generations: number[] = []
|
|
293
|
+
connections$.subscribe((generation) => generations.push(generation))
|
|
294
|
+
heartbeats$.next(new Date())
|
|
295
|
+
|
|
296
|
+
const reconnected$ = new Subject<Date>()
|
|
297
|
+
mockBifurClient.heartbeats = reconnected$.asObservable()
|
|
298
|
+
heartbeats$.error(new Error('WebSocket connection error'))
|
|
299
|
+
|
|
300
|
+
vi.advanceTimersByTime(199)
|
|
301
|
+
reconnected$.next(new Date())
|
|
302
|
+
expect(generations).toEqual([1])
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe('incoming events across a reconnect', () => {
|
|
307
|
+
it('keeps delivering presence after the socket drops', () => {
|
|
308
|
+
// bifur errors every stream on the same socket close, the inbound listener
|
|
309
|
+
// included. If only the heartbeat stream recovers, this client re-announces
|
|
310
|
+
// itself to peers while permanently ceasing to see any of them.
|
|
311
|
+
const firstListen$ = new Subject<IncomingBifurEvent>()
|
|
312
|
+
const secondListen$ = new Subject<IncomingBifurEvent>()
|
|
313
|
+
mockBifurClient.listen.mockReturnValueOnce(firstListen$).mockReturnValueOnce(secondListen$)
|
|
314
|
+
|
|
315
|
+
const [incomingEvents$, , connections$] = createBifurTransport({
|
|
316
|
+
client: mockSanityClient,
|
|
317
|
+
token$,
|
|
318
|
+
sessionId: 'my-session',
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const received: TransportEvent[] = []
|
|
322
|
+
incomingEvents$.subscribe({
|
|
323
|
+
next: (event) => received.push(event),
|
|
324
|
+
error: () => received.push({type: 'rollCall', userId: 'STREAM_DIED', sessionId: ''}),
|
|
325
|
+
})
|
|
326
|
+
connections$.subscribe()
|
|
327
|
+
|
|
328
|
+
heartbeats$.next(new Date())
|
|
329
|
+
firstListen$.next({type: 'rollCall', i: 'user-1', session: 'before'})
|
|
330
|
+
expect(received.map((e) => e.sessionId)).toEqual(['before'])
|
|
331
|
+
|
|
332
|
+
// Socket closes: both streams error.
|
|
333
|
+
const reconnectedHeartbeats$ = new Subject<Date>()
|
|
334
|
+
mockBifurClient.heartbeats = reconnectedHeartbeats$.asObservable()
|
|
335
|
+
firstListen$.error(new Error('WebSocket connection error'))
|
|
336
|
+
heartbeats$.error(new Error('WebSocket connection error'))
|
|
337
|
+
|
|
338
|
+
vi.advanceTimersByTime(200)
|
|
339
|
+
reconnectedHeartbeats$.next(new Date())
|
|
340
|
+
|
|
341
|
+
// Peers answer the roll call we send on reconnect. We have to be listening.
|
|
342
|
+
secondListen$.next({type: 'rollCall', i: 'user-1', session: 'after'})
|
|
343
|
+
|
|
344
|
+
expect(received.map((e) => e.sessionId)).toEqual(['before', 'after'])
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('unload', () => {
|
|
349
|
+
afterEach(() => {
|
|
350
|
+
vi.unstubAllGlobals()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('emits on beforeunload and pagehide, and removes its listeners', () => {
|
|
354
|
+
// These tests run in Node, so stand in a bare EventTarget for `window`.
|
|
355
|
+
const fakeWindow = new EventTarget()
|
|
356
|
+
vi.stubGlobal('window', fakeWindow)
|
|
357
|
+
|
|
358
|
+
const [, , , unload$] = createBifurTransport({
|
|
359
|
+
client: mockSanityClient,
|
|
360
|
+
token$,
|
|
361
|
+
sessionId: 'my-session',
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
let emissions = 0
|
|
365
|
+
const subscription = unload$.subscribe(() => {
|
|
366
|
+
emissions += 1
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
fakeWindow.dispatchEvent(new Event('beforeunload'))
|
|
370
|
+
expect(emissions).toBe(1)
|
|
371
|
+
|
|
372
|
+
// `pagehide` matters on its own: iOS Safari and pages entering the
|
|
373
|
+
// back/forward cache never fire `beforeunload`.
|
|
374
|
+
fakeWindow.dispatchEvent(new Event('pagehide'))
|
|
375
|
+
expect(emissions).toBe(2)
|
|
376
|
+
|
|
377
|
+
// The listeners belong to the subscription, so unsubscribing removes them
|
|
378
|
+
// instead of leaking one per transport for the life of the page.
|
|
379
|
+
subscription.unsubscribe()
|
|
380
|
+
fakeWindow.dispatchEvent(new Event('beforeunload'))
|
|
381
|
+
expect(emissions).toBe(2)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('is inert when there is no window', () => {
|
|
385
|
+
const [, , , unload$] = createBifurTransport({
|
|
386
|
+
client: mockSanityClient,
|
|
387
|
+
token$,
|
|
388
|
+
sessionId: 'my-session',
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
let completed = false
|
|
392
|
+
unload$.subscribe({complete: () => (completed = true)})
|
|
393
|
+
expect(completed).toBe(true)
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
describe('dispatch', () => {
|
|
206
398
|
it('sends a "rollCall" message', () => {
|
|
207
399
|
const [, dispatchMessage] = createBifurTransport({
|
|
208
400
|
client: mockSanityClient,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type BifurClient, fromUrl} from '@sanity/bifur-client'
|
|
2
2
|
import {type SanityClient} from '@sanity/client'
|
|
3
|
-
import {EMPTY, fromEvent, type Observable} from 'rxjs'
|
|
4
|
-
import {map, share, switchMap} from 'rxjs/operators'
|
|
3
|
+
import {defer, EMPTY, fromEvent, merge, type Observable, timer} from 'rxjs'
|
|
4
|
+
import {catchError, distinctUntilChanged, map, retry, share, switchMap} from 'rxjs/operators'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
type BifurTransportOptions,
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
type TransportMessage,
|
|
12
12
|
} from './types'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Longest gap between reconnection attempts, matching the Studio's presence
|
|
16
|
+
* transport so both behave the same way on a flaky connection.
|
|
17
|
+
*/
|
|
18
|
+
const CONNECT_RETRY_MAX_DELAY = 1000 * 240
|
|
19
|
+
|
|
14
20
|
type BifurStateMessage = {
|
|
15
21
|
type: 'state'
|
|
16
22
|
i: string
|
|
@@ -36,8 +42,25 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
|
|
|
36
42
|
|
|
37
43
|
function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
|
|
38
44
|
const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
|
|
39
|
-
const {
|
|
40
|
-
|
|
45
|
+
const {
|
|
46
|
+
resource,
|
|
47
|
+
dataset,
|
|
48
|
+
url: baseUrl,
|
|
49
|
+
requestTagPrefix = 'sanity.sdk.presence',
|
|
50
|
+
} = bifurVersionedClient.config()
|
|
51
|
+
|
|
52
|
+
let resourcePath: string
|
|
53
|
+
if (resource?.type === 'canvas') {
|
|
54
|
+
resourcePath = `canvases/${resource.id}`
|
|
55
|
+
} else if (dataset) {
|
|
56
|
+
// Dataset clients use project hostname — dataset name alone is the socket path
|
|
57
|
+
resourcePath = dataset
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`Unable to determine presence URL: no canvas resource or dataset configured`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const url = `${baseUrl}/socket/${resourcePath}`.replace(/^http/, 'ws')
|
|
63
|
+
|
|
41
64
|
const urlWithTag = `${url}?tag=${requestTagPrefix}`
|
|
42
65
|
|
|
43
66
|
return fromUrl(urlWithTag, {token$})
|
|
@@ -74,13 +97,80 @@ const handleIncomingMessage = (event: IncomingBifurEvent): TransportEvent => {
|
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Emits an incrementing generation number each time the socket becomes live.
|
|
102
|
+
*
|
|
103
|
+
* `@sanity/bifur-client` does not reconnect on its own — it errors the stream
|
|
104
|
+
* when the socket closes. Without the `retry` below, presence stops for the
|
|
105
|
+
* life of the page after the first dropped connection.
|
|
106
|
+
*/
|
|
107
|
+
const createConnections = (bifur: BifurClient): Observable<number> => {
|
|
108
|
+
let generation = 0
|
|
109
|
+
|
|
110
|
+
return defer(() => {
|
|
111
|
+
const current = ++generation
|
|
112
|
+
// `heartbeats` emits as soon as the socket is open and authorized, then on
|
|
113
|
+
// every server heartbeat and every response. Subscribing to it is also what
|
|
114
|
+
// keeps the socket alive between requests.
|
|
115
|
+
return bifur.heartbeats.pipe(map(() => current))
|
|
116
|
+
}).pipe(
|
|
117
|
+
retry({
|
|
118
|
+
delay: (_error, retryCount) =>
|
|
119
|
+
timer(Math.min(CONNECT_RETRY_MAX_DELAY, 2 ** retryCount * 100)),
|
|
120
|
+
resetOnSuccess: true,
|
|
121
|
+
}),
|
|
122
|
+
// After `retry` so that one operator instance spans reconnects, making each
|
|
123
|
+
// live socket yield exactly one emission.
|
|
124
|
+
distinctUntilChanged(),
|
|
125
|
+
share(),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Presence events from everyone else in the room, re-established on every live
|
|
131
|
+
* connection.
|
|
132
|
+
*
|
|
133
|
+
* A socket close errors this stream just as it errors the heartbeats, so without
|
|
134
|
+
* re-subscribing the client would keep announcing itself after a reconnect while
|
|
135
|
+
* never hearing from anyone again. Driving it from `connections$` rather than
|
|
136
|
+
* giving it its own `retry` keeps a single reconnect loop, instead of two
|
|
137
|
+
* independent backoffs racing over one refcounted socket.
|
|
138
|
+
*/
|
|
139
|
+
const createIncomingEvents = (
|
|
140
|
+
bifur: BifurClient,
|
|
141
|
+
connections$: Observable<number>,
|
|
142
|
+
): Observable<TransportEvent> =>
|
|
143
|
+
connections$.pipe(
|
|
144
|
+
switchMap(() =>
|
|
145
|
+
bifur.listen<IncomingBifurEvent>('presence').pipe(
|
|
146
|
+
// Let a dead listener go quietly. The next connection replaces it, and
|
|
147
|
+
// surfacing the error here would tear down the reconnect loop with it.
|
|
148
|
+
catchError(() => EMPTY),
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
map(handleIncomingMessage),
|
|
152
|
+
share(),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Emits when the page is going away. `pagehide` is needed alongside
|
|
157
|
+
* `beforeunload` because iOS Safari and pages entering the back/forward cache
|
|
158
|
+
* never fire `beforeunload`.
|
|
159
|
+
*/
|
|
160
|
+
const createUnload = (): Observable<void> => {
|
|
161
|
+
if (typeof window === 'undefined') return EMPTY
|
|
162
|
+
|
|
163
|
+
return merge(fromEvent(window, 'beforeunload'), fromEvent(window, 'pagehide')).pipe(
|
|
164
|
+
map(() => undefined),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
77
168
|
export const createBifurTransport = (options: BifurTransportOptions): PresenceTransport => {
|
|
78
169
|
const {client, token$, sessionId} = options
|
|
79
170
|
const bifur = getBifurClient(client, token$)
|
|
80
171
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
.pipe(map(handleIncomingMessage))
|
|
172
|
+
const connections$ = createConnections(bifur)
|
|
173
|
+
const incomingEvents$ = createIncomingEvents(bifur, connections$)
|
|
84
174
|
|
|
85
175
|
const dispatchMessage = (message: TransportMessage): Observable<void> => {
|
|
86
176
|
switch (message.type) {
|
|
@@ -98,11 +188,5 @@ export const createBifurTransport = (options: BifurTransportOptions): PresenceTr
|
|
|
98
188
|
}
|
|
99
189
|
}
|
|
100
190
|
|
|
101
|
-
|
|
102
|
-
fromEvent(window, 'beforeunload')
|
|
103
|
-
.pipe(switchMap(() => dispatchMessage({type: 'disconnect'})))
|
|
104
|
-
.subscribe()
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return [incomingEvents$.pipe(share()), dispatchMessage]
|
|
191
|
+
return [incomingEvents$, dispatchMessage, connections$, createUnload()]
|
|
108
192
|
}
|