-
Notifications
You must be signed in to change notification settings - Fork 0
fix #604: load config before the first MoQ derive on cold load #614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
4c24571
fix(ui): re-resolve MoQ server URL when config loads after samples
streamkit-devin dcf9ad6
fix(ui): re-resolve from pending edit and make config re-resolve one-β¦
streamkit-devin b1bcd25
Merge branch 'main' into devin/1782066386-fix-604-moq-server-url
streamer45 a92a630
refactor(ui): await config before first MoQ derive on cold load
streamkit-devin 7738a03
refactor(ui): sequence config load inside loadAndApplySamples
streamkit-devin ddb2d06
fix(ui): prevent template clobber and align test mock in loadAndApplyβ¦
streamkit-devin 76d1979
refactor(ui): extract loadAndApplySamples to its own module
streamkit-devin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // SPDX-FileCopyrightText: Β© 2025 StreamKit Contributors | ||
| // | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import type { useStreamViewState } from '@/hooks/useStreamViewState'; | ||
| import { useStreamStore } from '@/stores/streamStore'; | ||
| import type { SamplePipeline } from '@/types/generated/api-types'; | ||
|
|
||
| import { loadAndApplySamples } from './streamSamples'; | ||
|
|
||
| // streamSamples imports the stream store, which pulls in the WebTransport-backed | ||
| // @moq/* libraries; stub them so the module loads in jsdom. | ||
| vi.mock('@moq/hang', () => ({ default: {} })); | ||
| vi.mock('@moq/watch', () => ({ default: {}, Broadcast: vi.fn() })); | ||
| vi.mock('@moq/publish', () => ({ default: {}, Broadcast: vi.fn() })); | ||
| vi.mock('@moq/signals', () => ({ Effect: vi.fn() })); | ||
|
|
||
| const listDynamicSamples = vi.fn(); | ||
| vi.mock('@/services/samples', () => ({ | ||
| listDynamicSamples: () => listDynamicSamples(), | ||
| })); | ||
|
|
||
| const sample = (id: string): SamplePipeline => | ||
| ({ id, name: id, yaml: `client:\n gateway_path: /moq/${id}\n` }) as SamplePipeline; | ||
|
|
||
| function makeViewState(): ReturnType<typeof useStreamViewState> { | ||
| return { | ||
| selectedTemplateId: '', | ||
| setSamples: vi.fn(), | ||
| setSamplesLoading: vi.fn(), | ||
| setSamplesError: vi.fn(), | ||
| setSelectedTemplateId: vi.fn(), | ||
| setPipelineYaml: vi.fn(), | ||
| } as unknown as ReturnType<typeof useStreamViewState>; | ||
| } | ||
|
|
||
| describe('loadAndApplySamples (cold-load ordering)', () => { | ||
| beforeEach(() => { | ||
| listDynamicSamples.mockReset(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| useStreamStore.setState({ configLoaded: false }); | ||
| }); | ||
|
|
||
| it('defers the first derive until config finishes loading', async () => { | ||
| listDynamicSamples.mockResolvedValue([sample('a')]); | ||
| const viewState = makeViewState(); | ||
| const deriveMoqFromYaml = vi.fn(); | ||
|
|
||
| let resolveConfig!: () => void; | ||
| const loadConfig = vi.fn( | ||
| () => | ||
| new Promise<void>((resolve) => { | ||
| resolveConfig = () => resolve(); | ||
| }) | ||
| ); | ||
| useStreamStore.setState({ configLoaded: false, loadConfig }); | ||
|
|
||
| const done = loadAndApplySamples(viewState, deriveMoqFromYaml); | ||
|
|
||
| // The sample list resolves first, but nothing is applied until config | ||
| // loads, so the derive never resolves against an empty configServerUrl (#604). | ||
| await Promise.resolve(); | ||
| expect(loadConfig).toHaveBeenCalledTimes(1); | ||
| expect(viewState.setPipelineYaml).not.toHaveBeenCalled(); | ||
| expect(deriveMoqFromYaml).not.toHaveBeenCalled(); | ||
|
|
||
| resolveConfig(); | ||
| await done; | ||
| expect(viewState.setPipelineYaml).toHaveBeenCalledWith(sample('a').yaml); | ||
| expect(deriveMoqFromYaml).toHaveBeenCalledWith(sample('a').yaml); | ||
| }); | ||
|
|
||
| it('does not reload config when it is already loaded (warm load)', async () => { | ||
| listDynamicSamples.mockResolvedValue([sample('a')]); | ||
| const viewState = makeViewState(); | ||
| const deriveMoqFromYaml = vi.fn(); | ||
|
|
||
| const loadConfig = vi.fn(() => Promise.resolve()); | ||
| useStreamStore.setState({ configLoaded: true, loadConfig }); | ||
|
|
||
| await loadAndApplySamples(viewState, deriveMoqFromYaml); | ||
|
|
||
| expect(loadConfig).not.toHaveBeenCalled(); | ||
| expect(deriveMoqFromYaml).toHaveBeenCalledWith(sample('a').yaml); | ||
| }); | ||
|
|
||
| it('reports an error and stops loading when the sample fetch fails', async () => { | ||
| listDynamicSamples.mockRejectedValue(new Error('boom')); | ||
| const viewState = makeViewState(); | ||
| const deriveMoqFromYaml = vi.fn(); | ||
|
|
||
| useStreamStore.setState({ configLoaded: true, loadConfig: vi.fn() }); | ||
|
|
||
| await loadAndApplySamples(viewState, deriveMoqFromYaml); | ||
|
|
||
| expect(viewState.setSamplesError).toHaveBeenCalledWith('boom'); | ||
| expect(viewState.setSamplesLoading).toHaveBeenLastCalledWith(false); | ||
| expect(deriveMoqFromYaml).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // SPDX-FileCopyrightText: Β© 2025 StreamKit Contributors | ||
| // | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| import type { useStreamViewState } from '@/hooks/useStreamViewState'; | ||
| import { listDynamicSamples } from '@/services/samples'; | ||
| import { useStreamStore } from '@/stores/streamStore'; | ||
| import { getLogger } from '@/utils/logger'; | ||
| import { orderSamplePipelinesSystemFirst } from '@/utils/samplePipelineOrdering'; | ||
|
|
||
| const logger = getLogger('streamSamples'); | ||
|
|
||
| /** | ||
| * Load dynamic pipeline samples and auto-select the first one. | ||
| * | ||
| * The first derive resolves the MoQ server URL from `configServerUrl`, which | ||
| * `loadConfig()` populates asynchronously. Config and samples are fetched in | ||
| * parallel (`Promise.all`), so the auto-select applies the template and derives | ||
| * its MoQ settings synchronously once config is loaded β the URL is present even | ||
| * when the sample list resolves first (otherwise the field stays blank on a cold | ||
| * load until the user edits the client section, issue #604). | ||
| */ | ||
| export async function loadAndApplySamples( | ||
| viewState: ReturnType<typeof useStreamViewState>, | ||
| deriveMoqFromYaml: (yaml: string) => void | ||
| ): Promise<void> { | ||
| const { configLoaded, loadConfig } = useStreamStore.getState(); | ||
| try { | ||
| viewState.setSamplesLoading(true); | ||
| viewState.setSamplesError(null); | ||
| const [, samples] = await Promise.all([ | ||
| configLoaded ? Promise.resolve() : loadConfig(), | ||
| listDynamicSamples(), | ||
| ]); | ||
| const orderedSamples = orderSamplePipelinesSystemFirst(samples); | ||
| viewState.setSamples(orderedSamples); | ||
|
|
||
| if (orderedSamples.length > 0 && !viewState.selectedTemplateId) { | ||
| const first = orderedSamples[0]; | ||
| viewState.setSelectedTemplateId(first.id); | ||
| viewState.setPipelineYaml(first.yaml); | ||
| deriveMoqFromYaml(first.yaml); | ||
| } | ||
| } catch (error) { | ||
| logger.error('Failed to load dynamic samples:', error); | ||
| viewState.setSamplesError( | ||
| error instanceof Error ? error.message : 'Failed to load pipeline templates' | ||
| ); | ||
| } finally { | ||
| viewState.setSamplesLoading(false); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Info: Config failure is silently absorbed by Promise.all β by design
Since
loadConfig()(ui/src/stores/streamStore.ts:240-264) has an internal try/catch and never rejects, a config fetch failure won't cause thePromise.allatui/src/views/streamSamples.ts:31-34to reject. Instead,configLoadedis set totruewith an emptyconfigServerUrland anerrorMessagein the store. ThederiveMoqFromYamlcall at line 42 will then resolve the server URL against an emptyconfigServerUrl, resulting in no URL being set (resolveServerUrlatui/src/utils/moqPeerSettings.ts:158returnsundefinedwhenconfigServerUrlis empty). The user sees the store's error message and can manually enter a URL. This is correct behavior but worth understanding β a config failure doesn't block sample display.Was this helpful? React with π or π to provide feedback.
Debug
Playground