Compare commits

...

1 Commits

Author SHA1 Message Date
Glary-Bot
0b88966d92 fix: remove safe legacy code (V1 draft fallbacks, migration module, topbar-container)
Remove dual-variant legacy code confirmed safe via extension codesearch
(zero extension dependencies):

- Remove V1 draft storage fallbacks (sessionStorage/localStorage) from
  workflowDraftStoreV2 — V2 persistence is now the sole path
- Delete V1→V2 draft migration module (migrateV1toV2.ts + tests) —
  migration period complete, V2 has been in production since Feb 2026
- Remove legacy topbar-container DOM injection support from
  TopMenuSection.vue (MutationObserver, onMounted append, etc.)
- Update stale header comment in useWorkflowPersistenceV2.ts

Items #5 (deprecated workspaceStore sidebar functions) and #14 (node def
schema migration) are excluded — #5 is used as the ExtensionManager
interface and #14 has 10+ consumers beyond nodeDefStore.
2026-04-20 02:44:35 +00:00
7 changed files with 4 additions and 701 deletions

View File

@@ -152,16 +152,6 @@ function createWrapper({
return { container, unmount, user }
}
function getLegacyCommandsContainer(container: Element): HTMLElement {
const legacyContainer = container.querySelector(
'[data-testid="legacy-topbar-container"]'
)
if (!(legacyContainer instanceof HTMLElement)) {
throw new Error('Expected legacy commands container to be present')
}
return legacyContainer
}
function createJob(id: string, status: JobStatus): JobListItem {
return {
id,
@@ -542,73 +532,4 @@ describe('TopMenuSection', () => {
expect(container.querySelector('span.bg-red-500')).not.toBeNull()
})
it('coalesces legacy topbar mutation scans to one check per frame', async () => {
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
const rafCallbacks: FrameRequestCallback[] = []
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
rafCallbacks.push(cb)
return rafCallbacks.length
})
vi.stubGlobal('cancelAnimationFrame', vi.fn())
const pinia = createTestingPinia({ createSpy: vi.fn })
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.UseNewMenu') return 'Top'
if (key === 'Comfy.UI.TabBarLayout') return 'Integrated'
if (key === 'Comfy.RightSidePanel.IsOpen') return true
return undefined
})
const { container, unmount } = createWrapper({
pinia,
attachTo: document.body
})
try {
await nextTick()
const actionbarContainer = container.querySelector('.actionbar-container')
expect(actionbarContainer).not.toBeNull()
expect(actionbarContainer!.classList).toContain('w-0')
const legacyContainer = getLegacyCommandsContainer(container)
const querySpy = vi.spyOn(legacyContainer, 'querySelector')
if (rafCallbacks.length > 0) {
const initialCallbacks = [...rafCallbacks]
rafCallbacks.length = 0
initialCallbacks.forEach((callback) => callback(0))
await nextTick()
}
querySpy.mockClear()
querySpy.mockReturnValue(document.createElement('div'))
for (let index = 0; index < 3; index++) {
const outer = document.createElement('div')
const inner = document.createElement('div')
inner.textContent = `legacy-${index}`
outer.appendChild(inner)
legacyContainer.appendChild(outer)
}
await vi.waitFor(() => {
expect(rafCallbacks.length).toBeGreaterThan(0)
})
expect(querySpy).not.toHaveBeenCalled()
const callbacks = [...rafCallbacks]
rafCallbacks.length = 0
callbacks.forEach((callback) => callback(0))
await nextTick()
expect(querySpy).toHaveBeenCalledTimes(1)
expect(actionbarContainer!.classList).toContain('px-2')
} finally {
unmount()
vi.unstubAllGlobals()
}
})
})

View File

@@ -36,12 +36,6 @@
<div ref="actionbarContainerRef" :class="actionbarContainerClass">
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
data-testid="legacy-topbar-container"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar
:top-menu-container="actionbarContainerRef"
@@ -127,9 +121,9 @@
</template>
<script setup lang="ts">
import { useLocalStorage, useMutationObserver } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
@@ -148,7 +142,6 @@ import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
import { useQueueUIStore } from '@/stores/queueStore'
@@ -196,7 +189,6 @@ const isActionbarFloating = computed(
*/
const hasDockedButtons = computed(() => {
if (actionBarButtonStore.buttons.length > 0) return true
if (hasLegacyContent.value) return true
if (isLoggedIn.value && !isIntegratedTabBar.value) return true
if (isDesktop && !isIntegratedTabBar.value) return true
if (isCloud && flags.workflowSharingEnabled) return true
@@ -282,51 +274,6 @@ const rightSidePanelTooltipConfig = computed(() =>
buildTooltipConfig(t('rightSidePanel.togglePanel'))
)
// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()
const hasLegacyContent = ref(false)
let legacyContentCheckRafId: number | null = null
function checkLegacyContent() {
const el = legacyCommandsContainerRef.value
if (!el) {
hasLegacyContent.value = false
return
}
// Mirror the CSS: [&:not(:has(*>*:not(:empty)))]:hidden
hasLegacyContent.value =
el.querySelector(':scope > * > *:not(:empty)') !== null
}
function scheduleLegacyContentCheck() {
if (legacyContentCheckRafId !== null) return
legacyContentCheckRafId = requestAnimationFrame(() => {
legacyContentCheckRafId = null
checkLegacyContent()
})
}
useMutationObserver(legacyCommandsContainerRef, scheduleLegacyContentCheck, {
childList: true,
subtree: true
})
onMounted(() => {
if (legacyCommandsContainerRef.value) {
app.menu.element.style.width = 'fit-content'
legacyCommandsContainerRef.value.appendChild(app.menu.element)
checkLegacyContent()
}
})
onBeforeUnmount(() => {
if (legacyContentCheckRafId === null) return
cancelAnimationFrame(legacyContentCheckRafId)
legacyContentCheckRafId = null
})
const openCustomNodeManager = async () => {
try {
await managerState.openManager({

View File

@@ -110,10 +110,6 @@ vi.mock('@/platform/distribution/types', () => ({
isCloud: false
}))
vi.mock('../migration/migrateV1toV2', () => ({
migrateV1toV2: vi.fn()
}))
type GraphChangedHandler = (() => void) | null
const mocks = vi.hoisted(() => {

View File

@@ -5,7 +5,6 @@
* - Uses V2 draft store with per-draft keys
* - Uses tab state composable for session pointers
* - Adds 512ms debounce on graph change persistence
* - Runs V1→V2 migration on first load
*/
import { debounce } from 'es-toolkit'
@@ -30,7 +29,6 @@ import {
} from '@/platform/workflow/management/stores/workflowStore'
import { PERSIST_DEBOUNCE_MS } from '../base/draftTypes'
import { clearAllV2Storage } from '../base/storageIO'
import { migrateV1toV2 } from '../migration/migrateV1toV2'
import { useWorkflowDraftStoreV2 } from '../stores/workflowDraftStoreV2'
import { useWorkflowTabState } from './useWorkflowTabState'
import { useSharedWorkflowUrlLoader } from '@/platform/workflow/sharing/composables/useSharedWorkflowUrlLoader'
@@ -53,9 +51,6 @@ export function useWorkflowPersistenceV2() {
const toast = useToast()
const { onUserLogout } = useCurrentUser()
// Run migration on module load, passing clientId for tab state migration
migrateV1toV2(undefined, api.clientId ?? api.initialClientId ?? undefined)
// Clear workflow persistence storage when user signs out (cloud only)
onUserLogout(() => {
if (isCloud) {

View File

@@ -1,322 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { hashPath } from '../base/hashUtil'
import { readOpenPaths } from '../base/storageIO'
import {
cleanupV1Data,
getMigrationStatus,
isV2MigrationComplete,
migrateV1toV2
} from './migrateV1toV2'
describe('migrateV1toV2', () => {
const workspaceId = 'test-workspace'
beforeEach(() => {
vi.resetModules()
localStorage.clear()
sessionStorage.clear()
})
afterEach(() => {
localStorage.clear()
sessionStorage.clear()
})
function setV1Data(
drafts: Record<
string,
{ data: string; updatedAt: number; name: string; isTemporary: boolean }
>,
order: string[]
) {
localStorage.setItem(
`Comfy.Workflow.Drafts:${workspaceId}`,
JSON.stringify(drafts)
)
localStorage.setItem(
`Comfy.Workflow.DraftOrder:${workspaceId}`,
JSON.stringify(order)
)
}
describe('isV2MigrationComplete', () => {
it('returns false when no V2 index exists', () => {
expect(isV2MigrationComplete(workspaceId)).toBe(false)
})
it('returns true when V2 index exists', () => {
localStorage.setItem(
`Comfy.Workflow.DraftIndex.v2:${workspaceId}`,
JSON.stringify({ v: 2, order: [], entries: {}, updatedAt: Date.now() })
)
expect(isV2MigrationComplete(workspaceId)).toBe(true)
})
})
describe('migrateV1toV2', () => {
it('returns -1 if V2 already exists', () => {
localStorage.setItem(
`Comfy.Workflow.DraftIndex.v2:${workspaceId}`,
JSON.stringify({ v: 2, order: [], entries: {}, updatedAt: Date.now() })
)
expect(migrateV1toV2(workspaceId)).toBe(-1)
})
it('creates empty V2 index if no V1 data', () => {
expect(migrateV1toV2(workspaceId)).toBe(0)
const indexJson = localStorage.getItem(
`Comfy.Workflow.DraftIndex.v2:${workspaceId}`
)
expect(indexJson).not.toBeNull()
const index = JSON.parse(indexJson!)
expect(index.v).toBe(2)
expect(index.order).toEqual([])
})
it('migrates V1 drafts to V2 format', () => {
const v1Drafts = {
'workflows/a.json': {
data: '{"nodes":[1]}',
updatedAt: 1000,
name: 'a',
isTemporary: true
},
'workflows/b.json': {
data: '{"nodes":[2]}',
updatedAt: 2000,
name: 'b',
isTemporary: false
}
}
setV1Data(v1Drafts, ['workflows/a.json', 'workflows/b.json'])
const migrated = migrateV1toV2(workspaceId)
expect(migrated).toBe(2)
// Check V2 index
const indexJson = localStorage.getItem(
`Comfy.Workflow.DraftIndex.v2:${workspaceId}`
)
const index = JSON.parse(indexJson!)
expect(index.order).toHaveLength(2)
// Check payloads
const keyA = hashPath('workflows/a.json')
const keyB = hashPath('workflows/b.json')
const payloadA = localStorage.getItem(
`Comfy.Workflow.Draft.v2:${workspaceId}:${keyA}`
)
const payloadB = localStorage.getItem(
`Comfy.Workflow.Draft.v2:${workspaceId}:${keyB}`
)
expect(payloadA).not.toBeNull()
expect(payloadB).not.toBeNull()
expect(JSON.parse(payloadA!).data).toBe('{"nodes":[1]}')
expect(JSON.parse(payloadB!).data).toBe('{"nodes":[2]}')
})
it('preserves LRU order during migration', () => {
const v1Drafts = {
'workflows/first.json': {
data: '{}',
updatedAt: 1000,
name: 'first',
isTemporary: true
},
'workflows/second.json': {
data: '{}',
updatedAt: 2000,
name: 'second',
isTemporary: true
},
'workflows/third.json': {
data: '{}',
updatedAt: 3000,
name: 'third',
isTemporary: true
}
}
setV1Data(v1Drafts, [
'workflows/first.json',
'workflows/second.json',
'workflows/third.json'
])
migrateV1toV2(workspaceId)
const indexJson = localStorage.getItem(
`Comfy.Workflow.DraftIndex.v2:${workspaceId}`
)
const index = JSON.parse(indexJson!)
// Order should be preserved (oldest to newest)
const expectedOrder = [
hashPath('workflows/first.json'),
hashPath('workflows/second.json'),
hashPath('workflows/third.json')
]
expect(index.order).toEqual(expectedOrder)
})
it('keeps V1 data intact after migration', () => {
const v1Drafts = {
'workflows/test.json': {
data: '{}',
updatedAt: 1000,
name: 'test',
isTemporary: true
}
}
setV1Data(v1Drafts, ['workflows/test.json'])
migrateV1toV2(workspaceId)
// V1 data should still exist
expect(
localStorage.getItem(`Comfy.Workflow.Drafts:${workspaceId}`)
).not.toBeNull()
expect(
localStorage.getItem(`Comfy.Workflow.DraftOrder:${workspaceId}`)
).not.toBeNull()
})
})
describe('cleanupV1Data', () => {
it('removes V1 keys', () => {
setV1Data(
{
'workflows/test.json': {
data: '{}',
updatedAt: 1,
name: 'test',
isTemporary: true
}
},
['workflows/test.json']
)
cleanupV1Data(workspaceId)
expect(
localStorage.getItem(`Comfy.Workflow.Drafts:${workspaceId}`)
).toBeNull()
expect(
localStorage.getItem(`Comfy.Workflow.DraftOrder:${workspaceId}`)
).toBeNull()
})
})
describe('V1 tab state migration', () => {
it('migrates V1 tab state pointers to V2 format', () => {
// Simulate V1 state: user had 3 workflows open, 2nd was active
const v1Drafts = {
'workflows/a.json': {
data: '{"nodes":[1]}',
updatedAt: 1000,
name: 'a',
isTemporary: true
},
'workflows/b.json': {
data: '{"nodes":[2]}',
updatedAt: 2000,
name: 'b',
isTemporary: true
},
'workflows/c.json': {
data: '{"nodes":[3]}',
updatedAt: 3000,
name: 'c',
isTemporary: false
}
}
setV1Data(v1Drafts, [
'workflows/a.json',
'workflows/b.json',
'workflows/c.json'
])
// V1 tab state stored by setStorageValue (localStorage fallback keys)
localStorage.setItem(
'Comfy.OpenWorkflowsPaths',
JSON.stringify([
'workflows/a.json',
'workflows/b.json',
'workflows/c.json'
])
)
localStorage.setItem('Comfy.ActiveWorkflowIndex', JSON.stringify(1))
// Run migration (simulating upgrade from pre-V2 to V2)
const clientId = 'client-123'
const result = migrateV1toV2(workspaceId, clientId)
expect(result).toBe(3)
// V2 tab state should be readable via the V2 API
const openPaths = readOpenPaths(clientId, workspaceId)
// This is the bug: V1 tab state is NOT migrated, so openPaths is null
expect(openPaths).not.toBeNull()
expect(openPaths!.paths).toEqual([
'workflows/a.json',
'workflows/b.json',
'workflows/c.json'
])
expect(openPaths!.activeIndex).toBe(1)
})
it('does not migrate tab state when V1 tab state keys are absent', () => {
const v1Drafts = {
'workflows/a.json': {
data: '{}',
updatedAt: 1000,
name: 'a',
isTemporary: true
}
}
setV1Data(v1Drafts, ['workflows/a.json'])
// No V1 tab state keys in localStorage
migrateV1toV2(workspaceId)
const openPaths = readOpenPaths('any-client-id', workspaceId)
// No tab state to migrate — should remain null
expect(openPaths).toBeNull()
})
})
describe('getMigrationStatus', () => {
it('reports correct status', () => {
setV1Data(
{
'workflows/a.json': {
data: '{}',
updatedAt: 1,
name: 'a',
isTemporary: true
},
'workflows/b.json': {
data: '{}',
updatedAt: 2,
name: 'b',
isTemporary: true
}
},
['workflows/a.json', 'workflows/b.json']
)
const status = getMigrationStatus(workspaceId)
expect(status.v1Exists).toBe(true)
expect(status.v2Exists).toBe(false)
expect(status.v1DraftCount).toBe(2)
expect(status.v2DraftCount).toBe(0)
})
})
})

View File

@@ -1,203 +0,0 @@
/**
* V1 to V2 Migration
*
* Migrates draft data from V1 blob format to V2 per-draft keys.
* Runs once on first load if V2 index doesn't exist.
* Keeps V1 data intact for rollback until 2026-07-15.
*/
import type { DraftIndexV2 } from '../base/draftTypes'
import { upsertEntry, createEmptyIndex } from '../base/draftCacheV2'
import { hashPath } from '../base/hashUtil'
import { getWorkspaceId } from '../base/storageKeys'
import {
readIndex,
writeIndex,
writeOpenPaths,
writePayload
} from '../base/storageIO'
/**
* V1 draft snapshot structure (from draftCache.ts)
*/
interface V1DraftSnapshot {
data: string
updatedAt: number
name: string
isTemporary: boolean
}
/**
* V1 storage keys - workspace-scoped blob format
*/
const V1_KEYS = {
drafts: (workspaceId: string) => `Comfy.Workflow.Drafts:${workspaceId}`,
order: (workspaceId: string) => `Comfy.Workflow.DraftOrder:${workspaceId}`,
openPaths: 'Comfy.OpenWorkflowsPaths',
activeIndex: 'Comfy.ActiveWorkflowIndex'
}
/**
* Checks if V2 migration has been completed for the current workspace.
*/
export function isV2MigrationComplete(workspaceId: string): boolean {
const v2Index = readIndex(workspaceId)
return v2Index !== null
}
/**
* Reads V1 drafts from localStorage.
*/
function readV1Drafts(
workspaceId: string
): { drafts: Record<string, V1DraftSnapshot>; order: string[] } | null {
try {
const draftsJson = localStorage.getItem(V1_KEYS.drafts(workspaceId))
const orderJson = localStorage.getItem(V1_KEYS.order(workspaceId))
if (!draftsJson) return null
const drafts = JSON.parse(draftsJson) as Record<string, V1DraftSnapshot>
const order = orderJson ? (JSON.parse(orderJson) as string[]) : []
return { drafts, order }
} catch {
return null
}
}
/**
* Migrates V1 drafts to V2 format.
*
* @returns Number of drafts migrated, or -1 if migration not needed/failed
*/
export function migrateV1toV2(
workspaceId: string = getWorkspaceId(),
clientId?: string
): number {
// Check if V2 already exists
if (isV2MigrationComplete(workspaceId)) {
return -1
}
// Read V1 data
const v1Data = readV1Drafts(workspaceId)
if (!v1Data) {
// No V1 data to migrate - create empty V2 index
if (!writeIndex(workspaceId, createEmptyIndex())) return -1
return 0
}
// Build V2 index and write payloads
let index: DraftIndexV2 = createEmptyIndex()
let migrated = 0
// Process in order (oldest first) to maintain LRU order
for (const path of v1Data.order) {
const draft = v1Data.drafts[path]
if (!draft) continue
const draftKey = hashPath(path)
// Write payload
const payloadWritten = writePayload(workspaceId, draftKey, {
data: draft.data,
updatedAt: draft.updatedAt
})
if (!payloadWritten) {
console.warn(`[V2 Migration] Failed to write payload for ${path}`)
continue
}
// Update index
const { index: newIndex } = upsertEntry(index, path, {
name: draft.name,
isTemporary: draft.isTemporary,
updatedAt: draft.updatedAt
})
index = newIndex
migrated++
}
// Write final index
if (!writeIndex(workspaceId, index)) {
console.error('[V2 Migration] Failed to write index')
return -1
}
// Migrate V1 tab state pointers to V2 sessionStorage format.
// V1 used setStorageValue which stored tab state in localStorage as fallback.
// V2 uses sessionStorage keyed by clientId. Without this migration,
// users upgrading from V1 lose their open tab list.
migrateV1TabState(workspaceId, clientId)
if (migrated > 0) {
console.warn(`[V2 Migration] Migrated ${migrated} drafts from V1 to V2`)
}
return migrated
}
/**
* Migrates V1 tab state (open paths + active index) to V2 format.
* V1 stored these in localStorage via setStorageValue fallback.
* V2 uses sessionStorage keyed by clientId.
*/
function migrateV1TabState(workspaceId: string, clientId?: string): void {
if (!clientId) return
try {
const pathsJson = localStorage.getItem(V1_KEYS.openPaths)
if (!pathsJson) return
const paths = JSON.parse(pathsJson)
if (!Array.isArray(paths) || paths.length === 0) return
const indexJson = localStorage.getItem(V1_KEYS.activeIndex)
let activeIndex = 0
if (indexJson !== null) {
const parsed = JSON.parse(indexJson)
if (typeof parsed === 'number' && Number.isFinite(parsed)) {
activeIndex = Math.min(Math.max(0, parsed), paths.length - 1)
}
}
writeOpenPaths(clientId, { workspaceId, paths, activeIndex })
} catch {
// Best effort - don't block draft migration on tab state errors
}
}
/**
* Cleans up V1 data after successful migration.
* Should NOT be called until 2026-07-15 to allow rollback.
*/
export function cleanupV1Data(workspaceId: string = getWorkspaceId()): void {
try {
localStorage.removeItem(V1_KEYS.drafts(workspaceId))
localStorage.removeItem(V1_KEYS.order(workspaceId))
console.warn('[V2 Migration] Cleaned up V1 data')
} catch {
// Ignore cleanup errors
}
}
/**
* Gets migration status for debugging.
*/
export function getMigrationStatus(workspaceId: string = getWorkspaceId()): {
v1Exists: boolean
v2Exists: boolean
v1DraftCount: number
v2DraftCount: number
} {
const v1Data = readV1Drafts(workspaceId)
const v2Index = readIndex(workspaceId)
return {
v1Exists: v1Data !== null,
v2Exists: v2Index !== null,
v1DraftCount: v1Data ? v1Data.order.length : 0,
v2DraftCount: v2Index ? v2Index.order.length : 0
}
}

View File

@@ -33,7 +33,6 @@ import {
writeIndex,
writePayload
} from '../base/storageIO'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
interface DraftMeta {
@@ -319,11 +318,7 @@ export const useWorkflowDraftStoreV2 = defineStore('workflowDraftV2', () => {
async function loadPersistedWorkflow(
options: LoadPersistedWorkflowOptions
): Promise<boolean> {
const {
workflowName,
preferredPath,
fallbackToLatestDraft = false
} = options
const { preferredPath, fallbackToLatestDraft = false } = options
// 1. Try preferred path
if (preferredPath && (await loadDraft(preferredPath))) {
@@ -338,33 +333,7 @@ export const useWorkflowDraftStoreV2 = defineStore('workflowDraftV2', () => {
}
}
// Legacy fallbacks are NOT workspace-scoped and must only be used for
// personal workspace to prevent cross-workspace data leakage.
// These exist only for migration from V1 and should be removed after 2026-07-15.
if (currentWorkspaceId() !== 'personal') {
return false
}
// 3. Legacy fallback: sessionStorage payload (remove after 2026-07-15)
const clientId = api.initialClientId ?? api.clientId
if (clientId) {
try {
const sessionPayload = sessionStorage.getItem(`workflow:${clientId}`)
if (await tryLoadGraph(sessionPayload, workflowName)) {
return true
}
} catch {
// Ignore storage access errors and continue fallback chain
}
}
// 4. Legacy fallback: localStorage payload (remove after 2026-07-15)
try {
const localPayload = localStorage.getItem('workflow')
return await tryLoadGraph(localPayload, workflowName)
} catch {
return false
}
return false
}
/**