| 1 | import { sessionIdentityBaseKey, sessionIdentityStableKey, type SessionIdentityRef } from "../lib/sessionIdentity"; |
| 2 | |
| 3 | export type SessionIdentityInput = { |
| 4 | tabId?: string; |
| 5 | session?: SessionIdentityRef | null; |
| 6 | /** TabMeta compatibility identity: remote tab metas publish the canonical |
| 7 | * session id here and never carry a SessionRef (desktop/remote_projects.go). */ |
| 8 | sessionId?: string; |
| 9 | remote?: { hostId: string } | null; |
| 10 | sessionPath?: string; |
| 11 | sessionGeneration?: number; |
| 12 | scope?: string; |
| 13 | workspaceRoot?: string; |
| 14 | topicId?: string; |
| 15 | }; |
| 16 | |
| 17 | /** Runtime session identity; intentionally distinct from draft/workspace keys. */ |
| 18 | export function sessionIdentityKey(input: SessionIdentityInput): string { |
| 19 | const canonical = sessionIdentityStableKey(input); |
| 20 | if (canonical) return canonical; |
| 21 | // One remote tab rotates through many canonical sessions, so the id must |
| 22 | // fence them apart even when the tab, topic and path are reused. The |
| 23 | // generation is deliberately excluded here: a remote tab's generation is |
| 24 | // its event-pump reconnect counter, not a session identity. |
| 25 | const sessionId = input.sessionId?.trim(); |
| 26 | if (sessionId) return sessionIdentityBaseKey({ session: { hostId: input.remote?.hostId || "local", sessionId } }); |
| 27 | return [ |
| 28 | "topic", |
| 29 | input.scope ?? "", |
| 30 | input.workspaceRoot ?? "", |
| 31 | input.topicId ?? "", |
| 32 | input.tabId ?? "", |
| 33 | ].join("\u0000"); |
| 34 | } |
| 35 | |
| 36 | export type SessionSurfaceOwnership = Readonly<{ |
| 37 | revision: number; |
| 38 | tabId: string; |
| 39 | sessionKey: string; |
| 40 | }>; |
| 41 | |
| 42 | /** Commit-owned UI fence; A → B → A advances revision and never revives A. */ |
| 43 | export function createSessionSurfaceFence() { |
| 44 | let revision = 0; |
| 45 | let current: SessionSurfaceOwnership | undefined; |
| 46 | const owns = (ownership: SessionSurfaceOwnership): boolean => Boolean( |
| 47 | current |
| 48 | && current.revision === ownership.revision |
| 49 | && current.tabId === ownership.tabId |
| 50 | && current.sessionKey === ownership.sessionKey, |
| 51 | ); |
| 52 | return { |
| 53 | commit(tabId: string | undefined, sessionKey: string): SessionSurfaceOwnership | undefined { |
| 54 | if (!tabId) { |
| 55 | if (current) revision += 1; |
| 56 | current = undefined; |
| 57 | return undefined; |
| 58 | } |
| 59 | if (!current || current.tabId !== tabId || current.sessionKey !== sessionKey) revision += 1; |
| 60 | current = Object.freeze({ revision, tabId, sessionKey }); |
| 61 | return current; |
| 62 | }, |
| 63 | capture(): SessionSurfaceOwnership | undefined { |
| 64 | return current; |
| 65 | }, |
| 66 | owns, |
| 67 | ownsUnknown(ownership: unknown): boolean { |
| 68 | if (!ownership || typeof ownership !== "object") return false; |
| 69 | return owns(ownership as SessionSurfaceOwnership); |
| 70 | }, |
| 71 | dispose(): void { |
| 72 | revision += 1; |
| 73 | current = undefined; |
| 74 | }, |
| 75 | }; |
| 76 | } |
| 77 |