Files
windmill/frontend/src/lib/forkParentMemory.ts
T
Diego Imbert 32994df427 fix: return to parent workspace when a fork is deleted remotely (#9898)
* fix: return to parent workspace when a fork is deleted remotely

When a workspace fork was deleted remotely while a user had it open,
reloading stranded them: a regular member got logged out (whoami fails on
the vanished workspace) and a superadmin silently landed on a dead
workspace whose requests 404. Detect the deleted fork on load and redirect
to its parent (or the workspace picker) with a toast instead.

- forkParentMemory.ts: persist a bounded fork->parent map in localStorage
  while a fork is reachable (the parent is unrecoverable post-deletion).
- (logged) layout: record the current fork's parent via an effect.
- root layout: tryRecoverFromDeletedFork detects the vanished fork in
  loadUser and redirects to the remembered parent or the workspace picker,
  reusing the workspace list already fetched on mount.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: verify fork existence for superadmins before deleted-fork redirect

* fix: only recover deleted fork on actual 404 from superadmin check

* fix: recover prefixless dev-workspace forks via remembered parent

* fix: use workspace exists check to detect deleted forks

* fix: restore wm-fork- detection for non-member superadmin forks

* fix: record fork parent for non-member superadmin dev workspaces

* Update frontend/src/routes/(root)/+layout.svelte

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: store fork parent map with null-prototype to handle __proto__ ids

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-07-21 14:35:59 +00:00

83 lines
2.7 KiB
TypeScript

import type { UserWorkspace } from './stores'
// A fork's parent linkage lives only in its own `workspace` row. Once the fork
// is deleted remotely, `listUserWorkspaces` stops returning it and the parent is
// unrecoverable from the server. We therefore mirror `fork id -> parent id` into
// localStorage while the fork is still reachable, so that after a reload landing
// on a now-deleted fork we can send the user back to its parent instead of a
// dead workspace / forced logout.
const FORK_PARENTS_KEY = 'fork_parents'
const MAX_ENTRIES = 50
// A null-prototype map so workspace ids that collide with Object prototype members
// (`__proto__`, `constructor`, …) are stored as plain own properties rather than
// triggering prototype semantics on read/write.
function emptyMap(): Record<string, string> {
return Object.create(null)
}
function readMap(): Record<string, string> {
const map = emptyMap()
try {
const raw = localStorage.getItem(FORK_PARENTS_KEY)
if (!raw) return map
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object') return map
for (const key of Object.keys(parsed)) {
const value = (parsed as Record<string, unknown>)[key]
if (typeof value === 'string') map[key] = value
}
} catch (e) {
console.error('Could not read fork parent mapping', e)
}
return map
}
function writeMap(map: Record<string, string>): void {
try {
localStorage.setItem(FORK_PARENTS_KEY, JSON.stringify(map))
} catch (e) {
console.error('Could not persist fork parent mapping', e)
}
}
export function rememberForkParent(forkId: string, parentId: string): void {
const map = readMap()
if (map[forkId] === parentId) return
// Re-insert at the end so the oldest entries are the ones trimmed below.
delete map[forkId]
map[forkId] = parentId
const keys = Object.keys(map)
if (keys.length > MAX_ENTRIES) {
for (const k of keys.slice(0, keys.length - MAX_ENTRIES)) delete map[k]
}
writeMap(map)
}
export function getRememberedForkParent(forkId: string): string | undefined {
return readMap()[forkId]
}
export function forgetForkParent(forkId: string): void {
const map = readMap()
if (forkId in map) {
delete map[forkId]
writeMap(map)
}
}
// Records the parent of the current workspace when it is a fork still present in
// the user's workspace list. No-op otherwise, so it never clobbers a previously
// remembered parent when the fork has already disappeared.
export function recordForkParent(
workspaceId: string | undefined,
workspaces: UserWorkspace[]
): void {
if (!workspaceId) return
const ws = workspaces.find((w) => w.id === workspaceId)
if (ws?.parent_workspace_id) {
rememberForkParent(workspaceId, ws.parent_workspace_id)
}
}