[backport cloud/1.42] fix: migrate V1 tab state pointers during V1->V2 draft migration (#10007) (#10908)

Backport of #10007 to cloud/1.42. Clean cherry-pick.
This commit is contained in:
Christian Byrne
2026-04-06 15:05:12 -07:00
committed by GitHub
parent 81aad3ff86
commit 3ca9c8a4d8
3 changed files with 131 additions and 5 deletions

View File

@@ -53,8 +53,8 @@ export function useWorkflowPersistenceV2() {
const toast = useToast()
const { onUserLogout } = useCurrentUser()
// Run migration on module load
migrateV1toV2()
// 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(() => {

View File

@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { hashPath } from '../base/hashUtil'
import { readOpenPaths } from '../base/storageIO'
import {
cleanupV1Data,
getMigrationStatus,
@@ -212,6 +213,85 @@ describe('migrateV1toV2', () => {
})
})
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(

View File

@@ -10,7 +10,12 @@ 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, writePayload } from '../base/storageIO'
import {
readIndex,
writeIndex,
writeOpenPaths,
writePayload
} from '../base/storageIO'
/**
* V1 draft snapshot structure (from draftCache.ts)
@@ -27,7 +32,9 @@ interface V1DraftSnapshot {
*/
const V1_KEYS = {
drafts: (workspaceId: string) => `Comfy.Workflow.Drafts:${workspaceId}`,
order: (workspaceId: string) => `Comfy.Workflow.DraftOrder:${workspaceId}`
order: (workspaceId: string) => `Comfy.Workflow.DraftOrder:${workspaceId}`,
openPaths: 'Comfy.OpenWorkflowsPaths',
activeIndex: 'Comfy.ActiveWorkflowIndex'
}
/**
@@ -64,7 +71,10 @@ function readV1Drafts(
*
* @returns Number of drafts migrated, or -1 if migration not needed/failed
*/
export function migrateV1toV2(workspaceId: string = getWorkspaceId()): number {
export function migrateV1toV2(
workspaceId: string = getWorkspaceId(),
clientId?: string
): number {
// Check if V2 already exists
if (isV2MigrationComplete(workspaceId)) {
return -1
@@ -116,12 +126,48 @@ export function migrateV1toV2(workspaceId: string = getWorkspaceId()): number {
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.