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
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
import ReviewPipelineChanges from '$lib/components/pipelines/editor/ReviewPipelineChangesDialog.svelte'
import { parsePipelineDiff } from '$lib/functions/pipelines/pipelineDiff'
import { useToast } from '$lib/compositions/useToastNotification'
import { usePipelineAction } from '$lib/compositions/usePipelineAction.svelte'
import { getPipelineAction } from '$lib/compositions/usePipelineAction.svelte'
import type { editor } from 'monaco-editor/esm/vs/editor/editor.api'
import FocusBanner from '$lib/components/pipelines/editor/FocusBanner.svelte'
import StorageInUseBanner from '$lib/components/pipelines/editor/StorageInUseBanner.svelte'
Expand Down Expand Up @@ -91,10 +91,8 @@
nonNull(pipeline.current.status) && !isPipelineConfigEditable(pipeline.current.status)
)

const { updatePipelines, updatePipeline } = useUpdatePipelineList()
const pipelineAction = usePipelineAction()

const api = usePipelineManager()
const { updatePipelines } = useUpdatePipelineList()
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updatePipeline function was removed from destructuring but may still be needed elsewhere in the file. Verify that removing this doesn't break any functionality, as the original code imported both updatePipelines and updatePipeline.

Copilot uses AI. Check for mistakes.
const pipelineAction = getPipelineAction()
const pipelineActionCallbacks = usePipelineActionCallbacks()
const handleActionSuccess = async (pipelineName: string, action: PipelineAction) => {
const cbs = pipelineActionCallbacks.getAll(pipelineName, action)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,16 @@
}
$effect(() => {
pipelineName
untrack(() => pipelineActionCallbacks.add(pipelineName, 'start_paused', switchTo))
untrack(() => pipelineActionCallbacks.add(pipelineName, 'start', switchTo))
return () => {
pipelineActionCallbacks.remove(pipelineName, 'start_paused', switchTo)
pipelineActionCallbacks.remove(pipelineName, 'start', switchTo)
}
})
const forgetCurrentTab = async () => currentTab.remove()
$effect(() => {
untrack(() => pipelineActionCallbacks.add('', 'delete', forgetCurrentTab))
return () => {
pipelineActionCallbacks.remove('', 'start_paused', forgetCurrentTab)
pipelineActionCallbacks.remove('', 'delete', forgetCurrentTab)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this a bug?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@
delete changeStream[tenantName][pipelineName]
}
}

/**
* Check if a pipeline has any selected relations in the Change Stream tab.
* This is used to optimize pipeline start behavior - if no relations are selected,
* we can skip the pause-unpause sequence.
*/
export const hasSelectedRelations = (pipelineName: string): boolean => {
const tenantName = getSelectedTenant() || ''
const relations = pipelinesRelations[tenantName]?.[pipelineName]
if (!relations) {
return false
}
return Object.values(relations).some((r) => r.selected)
}
</script>

<script lang="ts">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
import Popup from '$lib/components/common/Popup.svelte'
import { slide } from 'svelte/transition'
import { useIsMobile } from '$lib/compositions/layout/useIsMobile.svelte'
import { usePipelineAction } from '$lib/compositions/usePipelineAction.svelte'
import { getPipelineAction } from '$lib/compositions/usePipelineAction.svelte'
import { usePipelineActionCallbacks } from '$lib/compositions/pipelines/usePipelineActionCallbacks.svelte'
import type { WritablePipeline } from '$lib/compositions/useWritablePipeline.svelte'

Expand Down Expand Up @@ -329,7 +329,7 @@ groups related actions into multi-action dropdowns when multiple options are ava
const basicBtnColor = 'preset-filled-surface-100-900'
const importantBtnColor = 'preset-filled-primary-500'

const { postPipelineAction } = usePipelineAction()
const { postPipelineAction } = getPipelineAction()
const pipelineActionCallbacks = usePipelineActionCallbacks()

const performStartAction = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { useToast } from '$lib/compositions/useToastNotification'
import { usePipelineManager } from '$lib/compositions/usePipelineManager.svelte'
import { usePremiumFeatures } from '$lib/compositions/usePremiumFeatures.svelte'
import { usePipelineAction } from '$lib/compositions/usePipelineAction.svelte'
import { getPipelineAction } from '$lib/compositions/usePipelineAction.svelte'
let {
pipelines,
selectedPipelines = $bindable()
Expand Down Expand Up @@ -117,7 +117,7 @@
selectedPipelines = []
}
const { toastError } = useToast()
const { postPipelineAction } = usePipelineAction()
const { postPipelineAction } = getPipelineAction()
let deletePipelines = () => {
selected.forEach(async (pipeline) => {
if (!isPipelineCodeEditable(pipeline.status)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type PipelineThumb } from '$lib/services/pipelineManager'
import { onMount } from 'svelte'
import { useToast } from '$lib/compositions/useToastNotification'
import { closedIntervalAction } from '$lib/functions/common/promise'
import { usePipelineManager, type PipelineManagerApi } from '../usePipelineManager.svelte'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// export const usePipelineAction = (api: PipelineManagerApi, pipelines: () => PipelineThumb[]) => {

import type { NamesInUnion } from '$lib/functions/common/union'
import type {
ExtendedPipeline,
Expand All @@ -13,6 +11,13 @@ import { useReactiveWaiter } from './useReactiveWaiter.svelte'
import { unionName } from '$lib/functions/common/union'
import { page } from '$app/state'
import { match } from 'ts-pattern'
import { hasSelectedRelations } from '$lib/components/pipelines/editor/TabChangeStream.svelte'

let postPipelineAction: (pipeline_name: string, action: PipelineAction, callbacks?: {
onPausedReady?: (pipelineName: string) => Promise<void>;
}) => Promise<{
waitFor: () => Promise<boolean>;
Comment on lines +16 to +19
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module-level postPipelineAction variable lacks documentation explaining why it needs to be a singleton and how the initialization pattern works. Consider adding a comment explaining that this is initialized once by usePipelineAction() and reused by getPipelineAction() to maintain waiter state across UI contexts.

Copilot uses AI. Check for mistakes.
}> = undefined!

/**
* Composition for handling pipeline actions with optimistic updates and state management.
Expand Down Expand Up @@ -64,8 +69,9 @@ export const usePipelineAction = () => {
'Replaying'
]
const reactiveWaiter = useReactiveWaiter(() => pipelineList.pipelines)
return {
postPipelineAction: async (
// We initialize a global state that will be reused across all calls to `getPipelineAction`.
// This allows keeping the same waiters alive while switching UI context
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know who the waiters are or what switching UI context means, but perhaps I have missed too many reviews of this code to understand

postPipelineAction ??= async (
pipeline_name: string,
action: PipelineAction | 'resume',
callbacks?: {
Expand Down Expand Up @@ -95,58 +101,67 @@ export const usePipelineAction = () => {

// Handle 'start' action with hidden paused intermediate state
if (action === 'start') {
// First start in paused state
await api.postPipelineAction(pipeline_name, 'start_paused')
// Optimization: Skip pause-unpause sequence if no relations are selected in Change Stream tab
const needsPauseUnpause = hasSelectedRelations(pipeline_name)

// Wait for paused state and run callbacks
const pausedWaiter = reactiveWaiter.createWaiter({
predicate: (ps) => {
const p = ps.find((p) => p.name === pipeline_name)
if (!p) {
throw new Error('Pipeline not found in pipelines list')
}
if (ignoreStatuses.includes(unionName(p.status))) {
return null
}
if (
(['Paused', 'AwaitingApproval'] satisfies PipelineStatus[]).findIndex(
(status) => status === p.status
) !== -1
) {
return { value: true }
}
if (p.status === 'Stopped') {
return { value: false }
if (needsPauseUnpause) {
// Relations are selected, use pause-unpause sequence to allow callbacks to run
// First start in paused state
await api.postPipelineAction(pipeline_name, 'start_paused')

// Wait for paused state and run callbacks
const pausedWaiter = reactiveWaiter.createWaiter({
predicate: (ps) => {
const p = ps.find((p) => p.name === pipeline_name)
if (!p) {
throw new Error('Pipeline not found in pipelines list')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the right thing to do?
can the users delete the pipelines (e.g., using the sdk) while this is happening?

}
if (ignoreStatuses.includes(unionName(p.status))) {
return null
}
if (
(['Paused', 'AwaitingApproval'] satisfies PipelineStatus[]).findIndex(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you really like to write code that is hard to understand

(status) => status === p.status
) !== -1
) {
return { value: true }
}
if (p.status === 'Stopped') {
return { value: false }
}
throw new Error(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if any of these errors happens no callbacks are run. is this what you expect?

`Unexpected status ${JSON.stringify(p.status)} while waiting for pipeline ${pipeline_name} to reach paused state`
)
}
throw new Error(
`Unexpected status ${JSON.stringify(p.status)} while waiting for pipeline ${pipeline_name} to reach paused state`
)
}
})
})

try {
const shouldContinue = await pausedWaiter.waitFor()
if (!shouldContinue) {
try {
const shouldContinue = await pausedWaiter.waitFor()
if (!shouldContinue) {
return {
waitFor: async () => {
// Pipeline was stopped before it could be resumed, listeners should not wait for running state
return false
}
}
}
} catch (e) {
return {
waitFor: async () => {
// Pipeline was stopped before it could be resumed, listeners should not wait for running state
return false
throw e
}
}
}
} catch (e) {
return {
waitFor: async () => {
throw e
}
}
}

updatePipeline(pipeline_name, (p) => ({ ...p, status: 'Initializing' }))
await callbacks?.onPausedReady?.(pipeline_name)
updatePipeline(pipeline_name, (p) => ({ ...p, status: 'Initializing' }))
await callbacks?.onPausedReady?.(pipeline_name)

// Then start normally
await api.postPipelineAction(pipeline_name, 'resume')
// Then start normally
await api.postPipelineAction(pipeline_name, 'resume')
} else {
// No relations selected, start directly without pausing first
await api.postPipelineAction(pipeline_name, 'start')
}
} else {
await api.postPipelineAction(pipeline_name, action)
}
Expand Down Expand Up @@ -205,5 +220,10 @@ export const usePipelineAction = () => {
}
}
}
}
}

export const getPipelineAction = () => {
return {
postPipelineAction
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
const isTablet = useIsTablet()
const { showPipelinesPanel: leftDrawer } = useLayoutSettings()
const pipelineList = usePipelineList(data.preloaded)
$effect.pre(() => {
// Refresh the pipeline list data when load function re-runs (e.g. tenant is changed)
pipelineList.pipelines = data.preloaded.pipelines
})
</script>

{@render children()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
import { useContextDrawer } from '$lib/compositions/layout/useContextDrawer.svelte'
import { getConfig } from '$lib/services/pipelineManager'
import { invalidateAll } from '$app/navigation'
import { usePipelineAction } from '$lib/compositions/usePipelineAction.svelte'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how this file works
how lovely, using parens and plus in file names.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can quickly go through it when you have the time. It's the syntax of the SvelteKit file-based router that resolves the URL in the browser to the Svelte page


const dialog = useGlobalDialog()

let { children, data }: { children: Snippet; data: LayoutData } = $props()

useRefreshPipelineList()
usePipelineAction()

const rightDrawer = useAdaptiveDrawer('right')
const contextDrawer = useContextDrawer()

Expand Down