Add shared instance metadata context

This commit is contained in:
Shantur Rathore
2025-12-15 00:42:16 +00:00
parent 8ec57da275
commit c8161669ac
6 changed files with 193 additions and 146 deletions

View File

@@ -0,0 +1,72 @@
import { Component, JSX, createContext, createEffect, createMemo, createSignal, useContext, type Accessor } from "solid-js"
import type { Instance } from "../../types/instance"
import { instances } from "../../stores/instances"
import { loadInstanceMetadata, hasMetadataLoaded } from "../hooks/use-instance-metadata"
interface InstanceMetadataContextValue {
isLoading: Accessor<boolean>
instance: Accessor<Instance>
metadata: Accessor<Instance["metadata"] | undefined>
refreshMetadata: () => Promise<void>
}
const InstanceMetadataContext = createContext<InstanceMetadataContextValue | null>(null)
interface InstanceMetadataProviderProps {
instance: Instance
children: JSX.Element
}
export const InstanceMetadataProvider: Component<InstanceMetadataProviderProps> = (props) => {
const resolvedInstance = createMemo(() => instances().get(props.instance.id) ?? props.instance)
const [isLoading, setIsLoading] = createSignal(true)
const ensureMetadata = async (force = false) => {
const current = resolvedInstance()
if (!current) {
setIsLoading(false)
return
}
if (!force && hasMetadataLoaded(current.metadata)) {
setIsLoading(false)
return
}
setIsLoading(true)
await loadInstanceMetadata(current, { force })
setIsLoading(false)
}
createEffect(() => {
const current = resolvedInstance()
// Ensure metadata becomes a dependency so we re-check when store updates
void current?.metadata
void ensureMetadata()
})
const contextValue: InstanceMetadataContextValue = {
isLoading,
instance: resolvedInstance,
metadata: () => resolvedInstance().metadata,
refreshMetadata: () => ensureMetadata(true),
}
return (
<InstanceMetadataContext.Provider value={contextValue}>
{props.children}
</InstanceMetadataContext.Provider>
)
}
export function useInstanceMetadataContext(): InstanceMetadataContextValue {
const ctx = useContext(InstanceMetadataContext)
if (!ctx) {
throw new Error("useInstanceMetadataContext must be used within InstanceMetadataProvider")
}
return ctx
}
export function useOptionalInstanceMetadataContext(): InstanceMetadataContextValue | null {
return useContext(InstanceMetadataContext)
}

View File

@@ -1,4 +1,3 @@
import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
import type { Instance, RawMcpStatus } from "../../types/instance"
import { fetchLspStatus, updateInstance } from "../../stores/instances"
import { getLogger } from "../../lib/logger"
@@ -6,87 +5,66 @@ import { getLogger } from "../../lib/logger"
const log = getLogger("session")
const pendingMetadataRequests = new Set<string>()
export function useInstanceMetadata(instanceAccessor: Accessor<Instance | undefined>) {
const [isLoading, setIsLoading] = createSignal(true)
function hasMetadataLoaded(metadata?: Instance["metadata"]): boolean {
if (!metadata) return false
return "project" in metadata && "mcpStatus" in metadata && "lspStatus" in metadata
}
createEffect(() => {
const instance = instanceAccessor()
if (!instance) {
setIsLoading(false)
return
export async function loadInstanceMetadata(instance: Instance, options?: { force?: boolean }): Promise<void> {
const client = instance.client
if (!client) {
log.warn("[metadata] Skipping fetch; client missing", { instanceId: instance.id })
return
}
if (!options?.force && hasMetadataLoaded(instance.metadata)) {
return
}
if (pendingMetadataRequests.has(instance.id)) {
return
}
pendingMetadataRequests.add(instance.id)
try {
const [projectResult, mcpResult, lspResult] = await Promise.allSettled([
client.project.current(),
client.mcp.status(),
fetchLspStatus(instance.id),
])
const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined
const mcpStatus = mcpResult.status === "fulfilled" ? (mcpResult.value.data as RawMcpStatus) : undefined
const lspStatus = lspResult.status === "fulfilled" ? lspResult.value ?? [] : undefined
const nextMetadata: Instance["metadata"] = {
...(instance.metadata ?? {}),
}
const instanceId = instance.id
const client = instance.client
const hasMetadata = Boolean(instance.metadata)
if (!client) {
setIsLoading(false)
pendingMetadataRequests.delete(instanceId)
return
if (projectResult.status === "fulfilled") {
nextMetadata.project = project ?? undefined
}
if (hasMetadata) {
setIsLoading(false)
pendingMetadataRequests.delete(instanceId)
return
if (mcpResult.status === "fulfilled") {
nextMetadata.mcpStatus = mcpStatus ?? nextMetadata.mcpStatus ?? {}
}
if (pendingMetadataRequests.has(instanceId)) {
setIsLoading(true)
return
if (lspResult.status === "fulfilled") {
nextMetadata.lspStatus = lspStatus ?? []
}
let cancelled = false
pendingMetadataRequests.add(instanceId)
setIsLoading(true)
if (!nextMetadata?.version && instance.binaryVersion) {
nextMetadata.version = instance.binaryVersion
}
void (async () => {
try {
const [projectResult, mcpResult, lspResult] = await Promise.allSettled([
client.project.current(),
client.mcp.status(),
fetchLspStatus(instanceId),
])
if (cancelled) {
return
}
const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined
const mcpStatus = mcpResult.status === "fulfilled" ? (mcpResult.value.data as RawMcpStatus) : undefined
const lspStatus = lspResult.status === "fulfilled" ? lspResult.value ?? [] : undefined
const nextMetadata: Instance["metadata"] = {
...(instance.metadata ?? {}),
...(project ? { project } : {}),
...(mcpStatus ? { mcpStatus } : {}),
...(lspStatus ? { lspStatus } : {}),
}
if (!nextMetadata?.version && instance.binaryVersion) {
nextMetadata.version = instance.binaryVersion
}
updateInstance(instanceId, { metadata: nextMetadata })
} catch (error) {
if (!cancelled) {
log.error("Failed to load instance metadata", error)
}
} finally {
pendingMetadataRequests.delete(instanceId)
if (!cancelled) {
setIsLoading(false)
}
}
})()
onCleanup(() => {
cancelled = true
})
})
return {
isLoading,
updateInstance(instance.id, { metadata: nextMetadata })
} catch (error) {
log.error("Failed to load instance metadata", error)
} finally {
pendingMetadataRequests.delete(instance.id)
}
}
export { hasMetadataLoaded }