Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions packages/workshop-frontend/src/BlueprintLandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export default function BlueprintLandingPage({ rpcStub }: Props) {
}
let cancelled = false
const accountMap = new Map<number, AccountOption>()
let subStub: { [Symbol.dispose](): void } | null = null

const subscriber = new AccountsSubscriberAdapter({
add({ id: accountId, description, vendor, supportedResources, credentialsValid, vendorId }) {
Expand All @@ -177,21 +176,15 @@ export default function BlueprintLandingPage({ rpcStub }: Props) {
},
})

authenticatedApi.subscribeConnectedAccounts(subscriber)
.then(stub => {
if (cancelled) {
stub[Symbol.dispose]()
} else {
subStub = stub
}
})
.catch(err => {
logRpcFailure('Failed to subscribe to connected accounts:', err)
})
const subscription = authenticatedApi.subscribeConnectedAccounts(subscriber)
subscription.catch(err => {
if (cancelled) return
logRpcFailure('Failed to subscribe to connected accounts:', err)
})

return () => {
cancelled = true
subStub?.[Symbol.dispose]()
subscription[Symbol.dispose]()
}
}, [isAuthenticated, authenticatedApi])

Expand Down
20 changes: 6 additions & 14 deletions packages/workshop-frontend/src/GatekeeperModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ export default function GatekeeperModal({
const spawnerEnvCandidatesRef = useRef(spawnerEnvCandidates)
spawnerEnvCandidatesRef.current = spawnerEnvCandidates

const accountSubscriptionRef = useRef<{ [Symbol.dispose](): void } | null>(null)
const configuratorFrameRef = useRef<ConfiguratorFrameState | null>(null)
const configuratorCollectResourceUrlRef = useRef<(() => Promise<string>) | null>(null)
const nextConfiguratorFrameKeyRef = useRef(0)
Expand Down Expand Up @@ -420,22 +419,15 @@ export default function GatekeeperModal({
setAccounts(Array.from(accountMap.values()))
},
})
authenticatedApi.subscribeConnectedAccounts(subscriber)
.then(stub => {
if (cancelled) {
stub[Symbol.dispose]()
} else {
accountSubscriptionRef.current = stub
}
})
.catch(error => {
logRpcFailure('Failed to subscribe to connected accounts:', error)
})
const subscription = authenticatedApi.subscribeConnectedAccounts(subscriber)
subscription.catch(error => {
if (cancelled) return
logRpcFailure('Failed to subscribe to connected accounts:', error)
})

return () => {
cancelled = true
accountSubscriptionRef.current?.[Symbol.dispose]()
accountSubscriptionRef.current = null
subscription[Symbol.dispose]()
}
}, [open, authenticatedApi])

Expand Down
27 changes: 24 additions & 3 deletions packages/workshop-frontend/src/ObserverConfigModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ function account(id: number, uniqueName: string, grantedResourceUrlPatterns?: st
}

type ApiOverrides = {
subscribeConnectedAccounts?: Mock<(
subscriber: ConnectedAccountsSubscriber,
) => Promise<{ [Symbol.dispose](): void }>>
connectAccount?: Mock<(vendorId: string, resourceUrlPatterns?: string[]) => Promise<{ url: string }>>
ensureAccountResources?: Mock<(
accountId: number,
Expand All @@ -92,13 +95,15 @@ function fakeApi(
overrides: ApiOverrides = {},
): RpcStub<AuthenticatedApi> {
return {
subscribeConnectedAccounts: async (subscriber: ConnectedAccountsSubscriber) => {
subscribeConnectedAccounts: overrides.subscribeConnectedAccounts ?? ((subscriber: ConnectedAccountsSubscriber) => {
for (const entry of accountEntries) {
subscriber.add(entry.id, entry.description, VENDOR, [DOC_RESOURCE], true, 'google')
}
subscriber.ready()
return { [Symbol.dispose]() {} }
},
return Object.assign(Promise.resolve({ [Symbol.dispose]() {} }), {
[Symbol.dispose]() {},
})
}),
listGatekeeperVendors: async () => [{
id: 'google',
description: VENDOR,
Expand Down Expand Up @@ -157,6 +162,22 @@ describe('ObserverConfigModal account selection', () => {
expect(rendered.querySelector('[data-testid="account-select"]')).toBeNull()
})

it('disposes a pending account subscription on unmount', async () => {
const dispose = vi.fn<() => void>()
const pendingSubscription = Object.assign(new Promise<{ [Symbol.dispose](): void }>(() => {}), {
[Symbol.dispose]: dispose,
})
const subscribeConnectedAccounts = vi.fn<
(subscriber: ConnectedAccountsSubscriber) => Promise<{ [Symbol.dispose](): void }>
>().mockReturnValue(pendingSubscription)
await render([], { api: fakeApi([], { subscribeConnectedAccounts }) })

act(() => root!.unmount())
root = undefined

expect(dispose).toHaveBeenCalledOnce()
})

it('keeps the account dropdown when multiple accounts match', async () => {
const rendered = await render([
account(1, 'dan@cloudflare.com'),
Expand Down
27 changes: 13 additions & 14 deletions packages/workshop-frontend/src/ObserverConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ export default function ObserverConfigModal({

// ── subscribe to the user's connected accounts ────────────────────────────────
useEffect(() => {
let subStub: { [Symbol.dispose](): void } | null = null
let cancelled = false

const subscriber = new AccountsSubscriberAdapter({
add({ id, description, vendor, supportedResources, credentialsValid, vendorId }) {
if (cancelled) return
setAccounts(prev => {
const next = new Map(prev)
next.set(id, { id, description, vendor, vendorId, supportedResources, credentialsValid })
Expand All @@ -121,6 +121,7 @@ export default function ObserverConfigModal({
}
},
remove(id) {
if (cancelled) return
setAccounts(prev => {
if (!prev.has(id)) return prev
const next = new Map(prev)
Expand All @@ -129,26 +130,24 @@ export default function ObserverConfigModal({
})
},
ready() {
if (cancelled) return
setReady(true)
},
})

authenticatedApi
.subscribeConnectedAccounts(subscriber, { includeForcedAutoProvisionedAccounts: true })
.then(stub => {
if (cancelled) { stub[Symbol.dispose](); return }
subStub = stub
})
.catch(err => {
// Loud on purpose: the modal has no retry path, so a quieted transient failure would
// strand the user on a permanent loader.
console.error('Failed to subscribe to connected accounts:', err)
toasts.add({ title: 'Failed to load your connected accounts', variant: 'error' })
})
const subscription = authenticatedApi.subscribeConnectedAccounts(
subscriber, { includeForcedAutoProvisionedAccounts: true })
subscription.catch(err => {
if (cancelled) return
// Loud on purpose: the modal has no retry path, so a quieted transient failure would
// strand the user on a permanent loader.
console.error('Failed to subscribe to connected accounts:', err)
toasts.add({ title: 'Failed to load your connected accounts', variant: 'error' })
})

return () => {
cancelled = true
subStub?.[Symbol.dispose]()
subscription[Symbol.dispose]()
}
}, [authenticatedApi])

Expand Down
19 changes: 6 additions & 13 deletions packages/workshop-frontend/src/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,16 @@ export default function OnboardingWizard({
}
},
})
let subscriptionStub: { [Symbol.dispose](): void } | null = null

authenticatedApi.subscribeConnectedAccounts(subscriber)
.then((stub) => {
if (cancelled) {
stub[Symbol.dispose]()
} else {
subscriptionStub = stub
}
})
.catch((err) => {
logRpcFailure('Failed to subscribe to connected accounts:', err)
})
const subscription = authenticatedApi.subscribeConnectedAccounts(subscriber)
subscription.catch((err) => {
if (cancelled) return
logRpcFailure('Failed to subscribe to connected accounts:', err)
})

return () => {
cancelled = true
subscriptionStub?.[Symbol.dispose]()
subscription[Symbol.dispose]()
}
}, [authenticatedApi])

Expand Down
13 changes: 3 additions & 10 deletions packages/workshop-frontend/src/components/ConnectionChips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export default function ConnectionChips() {

useEffect(() => {
let cancelled = false
let subscriptionStub: { [Symbol.dispose](): void } | null = null

const accountMap = new Map<number, ConnectedAccount>()

Expand All @@ -39,18 +38,12 @@ export default function ConnectionChips() {
if (!cancelled) setAccounts(Array.from(accountMap.values()))
},
})
const subPromise = authenticatedApi.subscribeConnectedAccounts(subscriber)
subPromise.then((stub) => {
if (cancelled) {
stub[Symbol.dispose]()
} else {
subscriptionStub = stub
}
}).catch(() => {})
const subscription = authenticatedApi.subscribeConnectedAccounts(subscriber)
subscription.catch(() => {})

return () => {
cancelled = true
subscriptionStub?.[Symbol.dispose]()
subscription[Symbol.dispose]()
}
}, [authenticatedApi])

Expand Down
25 changes: 8 additions & 17 deletions packages/workshop-frontend/src/routes/gatekeepers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { logRpcFailure } from '../rpcErrors'
import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useKumoToastManager } from '@cloudflare/kumo'
import {
MagnifyingGlass,
Expand Down Expand Up @@ -474,8 +474,6 @@ function ConnectorsPage() {
localStorage.setItem('gatekeepers-view', view)
}, [view])

const subscriptionRef = useRef<{ [Symbol.dispose](): void } | null>(null)

useEffect(() => {
let cancelled = false
const accountMap = new Map<number, AccountEntry>()
Expand Down Expand Up @@ -539,23 +537,16 @@ function ConnectorsPage() {
},
})

authenticatedApi.subscribeConnectedAccounts(subscriber)
.then((stub) => {
if (cancelled) {
stub[Symbol.dispose]()
} else {
subscriptionRef.current = stub
}
})
.catch((err) => {
logRpcFailure('Failed to subscribe to connected accounts:', err)
if (!cancelled) setLoadError(true)
})
const subscription = authenticatedApi.subscribeConnectedAccounts(subscriber)
subscription.catch((err) => {
if (cancelled) return
logRpcFailure('Failed to subscribe to connected accounts:', err)
setLoadError(true)
})

return () => {
cancelled = true
subscriptionRef.current?.[Symbol.dispose]()
subscriptionRef.current = null
subscription[Symbol.dispose]()
}
}, [authenticatedApi])

Expand Down
Loading