mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
Run local usage services in a supervised backend process (#40792)
* Move local usage services into a supervised backend process * Own usage recovery and worker retirement lifetimes * Preserve answered usage data during background refresh * Run independent usage scans concurrently in the backend * Limit backend startup paths to the delegated usage service * Show usage failures through backend recovery * Include usage failure strings in the English catalog * Keep usage update details beside refresh failures * Resolve usage freshness failure from the final producer snapshot * Retain the forced refresh completion requirement * Keep managed backend failures out of application crash reports * Preserve usage recovery boundaries across backend reconnects * Match the backend RPC cancellation signal in the usage seam proof * Avoid serializing backend requests before port delivery * Keep receiving request validation in the boundary schemas * test: align usage transport coverage with renderer project * fix: preserve unknown usage state and renderer retirement * fix(usage): revalidate tracking before scan admission * fix(usage): retry failed backend startup on demand * test(usage): isolate stale backend ownership checks
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
BackendConnectionListener,
|
||||
BackendConnectionPurpose
|
||||
} from '../../shared/backend-service-contract'
|
||||
import type { ClaudeUsageBreakdownKind, ClaudeUsageSnapshot } from '../../shared/claude-usage-types'
|
||||
import type { CodexUsageBreakdownKind, CodexUsageSnapshot } from '../../shared/codex-usage-types'
|
||||
import type {
|
||||
@@ -25,10 +29,14 @@ export type UsageQueryArgs<Snapshot extends UsageProviderSnapshot> = Pick<
|
||||
>
|
||||
|
||||
export type UsageProviderApi<Snapshot extends UsageProviderSnapshot, BreakdownKind> = {
|
||||
getScanState: () => Promise<Snapshot['scanState']>
|
||||
onConnectionChanged?: (listener: BackendConnectionListener) => () => void
|
||||
getScanState: (purpose?: BackendConnectionPurpose) => Promise<Snapshot['scanState']>
|
||||
setEnabled: (args: { enabled: boolean }) => Promise<Snapshot['scanState']>
|
||||
refresh: (args?: { force?: boolean }) => Promise<Snapshot['scanState']>
|
||||
getSnapshot: (args: UsageQueryArgs<Snapshot> & { limit?: number }) => Promise<Snapshot>
|
||||
getSnapshot: (
|
||||
args: UsageQueryArgs<Snapshot> & { limit?: number },
|
||||
purpose?: BackendConnectionPurpose
|
||||
) => Promise<Snapshot>
|
||||
getSummary: (args: UsageQueryArgs<Snapshot>) => Promise<Snapshot['summary']>
|
||||
getDaily: (args: UsageQueryArgs<Snapshot>) => Promise<Snapshot['daily']>
|
||||
getBreakdown: (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { backendUsageTransport } from '../backend-port-client'
|
||||
import { createUsageProviderApi } from '../usage-provider-api'
|
||||
import type { PreloadApi } from '../api-types'
|
||||
|
||||
export const claudeUsageApi = createUsageProviderApi(
|
||||
ipcRenderer,
|
||||
backendUsageTransport,
|
||||
'claudeUsage'
|
||||
) satisfies PreloadApi['claudeUsage']
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { backendUsageTransport } from '../backend-port-client'
|
||||
import { createUsageProviderApi } from '../usage-provider-api'
|
||||
import type { PreloadApi } from '../api-types'
|
||||
|
||||
export const codexUsageApi = createUsageProviderApi(
|
||||
ipcRenderer,
|
||||
backendUsageTransport,
|
||||
'codexUsage'
|
||||
) satisfies PreloadApi['codexUsage']
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { backendUsageTransport } from '../backend-port-client'
|
||||
import { createUsageProviderApi } from '../usage-provider-api'
|
||||
import type { PreloadApi } from '../api-types'
|
||||
|
||||
export const museUsageApi = createUsageProviderApi(
|
||||
ipcRenderer,
|
||||
backendUsageTransport,
|
||||
'museUsage'
|
||||
) satisfies PreloadApi['museUsage']
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { backendUsageTransport } from '../backend-port-client'
|
||||
import { createUsageProviderApi } from '../usage-provider-api'
|
||||
import type { PreloadApi } from '../api-types'
|
||||
|
||||
export const openCodeUsageApi = createUsageProviderApi(
|
||||
ipcRenderer,
|
||||
backendUsageTransport,
|
||||
'openCodeUsage'
|
||||
) satisfies PreloadApi['openCodeUsage']
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { BackendPortRpc, type BackendPort } from '../shared/backend-port-rpc'
|
||||
import {
|
||||
BACKEND_CONNECTION_CHANGED,
|
||||
BACKEND_PORT_DELIVERY,
|
||||
BACKEND_PORT_REQUEST,
|
||||
type BackendConnectionPurpose,
|
||||
type BackendConnectionStatus,
|
||||
type BackendConnectionListener
|
||||
} from '../shared/backend-service-contract'
|
||||
|
||||
let connection: BackendPortRpc | undefined
|
||||
let connecting: Promise<BackendPortRpc> | undefined
|
||||
type PortAttempt = {
|
||||
token: string
|
||||
purpose: BackendConnectionPurpose
|
||||
resolve(client: BackendPortRpc): void
|
||||
reject(error: Error): void
|
||||
}
|
||||
let pendingPort: PortAttempt | undefined
|
||||
const connectionListeners = new Set<BackendConnectionListener>()
|
||||
let connected: boolean | undefined
|
||||
let unavailable = false
|
||||
|
||||
function publishConnection(next: boolean): void {
|
||||
if (next) {
|
||||
unavailable = false
|
||||
}
|
||||
if (connected === next) {
|
||||
return
|
||||
}
|
||||
connected = next
|
||||
if (!next) {
|
||||
const previous = connection
|
||||
connection = undefined
|
||||
connecting = undefined
|
||||
const attempt = pendingPort
|
||||
pendingPort = undefined
|
||||
previous?.close()
|
||||
attempt?.reject(new Error('Usage disconnected. Try again after it reconnects.'))
|
||||
}
|
||||
for (const listener of connectionListeners) {
|
||||
listener(next)
|
||||
}
|
||||
}
|
||||
|
||||
ipcRenderer.on(BACKEND_PORT_DELIVERY, (event, data: { token?: string }) => {
|
||||
const [port, ...surplus] = event.ports
|
||||
for (const extra of surplus) {
|
||||
extra.close()
|
||||
}
|
||||
if (!port || !pendingPort || pendingPort.token !== data?.token) {
|
||||
port?.close()
|
||||
return
|
||||
}
|
||||
const adapter: BackendPort = {
|
||||
postMessage: (message) => port.postMessage(message),
|
||||
onMessage(listener) {
|
||||
const receive = (event: MessageEvent): void => listener(event.data)
|
||||
port.addEventListener('message', receive)
|
||||
port.start()
|
||||
return () => port.removeEventListener('message', receive)
|
||||
},
|
||||
onClose(listener) {
|
||||
port.addEventListener('close', listener)
|
||||
return () => port.removeEventListener('close', listener)
|
||||
},
|
||||
close: () => port.close()
|
||||
}
|
||||
const previous = connection
|
||||
connection = undefined
|
||||
previous?.close()
|
||||
const client = new BackendPortRpc(
|
||||
adapter,
|
||||
'renderer',
|
||||
async () => {
|
||||
throw new Error('Renderer capabilities are not available on this port.')
|
||||
},
|
||||
() => {
|
||||
if (connection === client) {
|
||||
publishConnection(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
connection = client
|
||||
const attempt = pendingPort
|
||||
pendingPort = undefined
|
||||
attempt.resolve(client)
|
||||
publishConnection(true)
|
||||
})
|
||||
|
||||
function requestPort(attempt: PortAttempt): void {
|
||||
void ipcRenderer
|
||||
.invoke(BACKEND_PORT_REQUEST, attempt.token, attempt.purpose)
|
||||
.catch((error: Error) => {
|
||||
if (pendingPort === attempt) {
|
||||
pendingPort = undefined
|
||||
attempt.reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function connect(purpose: BackendConnectionPurpose = 'demand'): Promise<BackendPortRpc> {
|
||||
if (connection) {
|
||||
return Promise.resolve(connection)
|
||||
}
|
||||
if (connecting) {
|
||||
if (purpose === 'demand' && pendingPort?.purpose === 'recovery') {
|
||||
pendingPort = { ...pendingPort, token: crypto.randomUUID(), purpose }
|
||||
requestPort(pendingPort)
|
||||
}
|
||||
return connecting
|
||||
}
|
||||
const attemptPromise = new Promise<BackendPortRpc>((resolve, reject) => {
|
||||
const attempt = { token: crypto.randomUUID(), purpose, resolve, reject }
|
||||
pendingPort = attempt
|
||||
requestPort(attempt)
|
||||
})
|
||||
const result = attemptPromise.finally(() => {
|
||||
if (connecting === result) {
|
||||
connecting = undefined
|
||||
}
|
||||
})
|
||||
connecting = result
|
||||
return result
|
||||
}
|
||||
|
||||
ipcRenderer.on(BACKEND_CONNECTION_CHANGED, (_event, state: BackendConnectionStatus) => {
|
||||
publishConnection(state.connected)
|
||||
if (!state.connected && state.unavailable && !unavailable) {
|
||||
unavailable = true
|
||||
for (const listener of connectionListeners) {
|
||||
listener(false, { unavailable: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const backendUsageTransport = {
|
||||
onConnectionChanged(listener: BackendConnectionListener): () => void {
|
||||
connectionListeners.add(listener)
|
||||
listener(connected ?? false, { initial: true, unavailable })
|
||||
return () => {
|
||||
connectionListeners.delete(listener)
|
||||
}
|
||||
},
|
||||
invoke: async (
|
||||
method: string,
|
||||
params?: unknown,
|
||||
purpose?: BackendConnectionPurpose
|
||||
): Promise<unknown> => (await connect(purpose)).call(method, params)
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { IpcRenderer } from 'electron'
|
||||
import type {
|
||||
BackendConnectionListener,
|
||||
BackendConnectionPurpose
|
||||
} from '../shared/backend-service-contract'
|
||||
import type { PreloadApi } from './api-types'
|
||||
|
||||
type UsageProviderApiKey = 'claudeUsage' | 'codexUsage' | 'openCodeUsage' | 'museUsage'
|
||||
@@ -6,24 +9,37 @@ type UsageProviderApi = PreloadApi[UsageProviderApiKey]
|
||||
type UsageRangeArgs = { scope: string; range: string }
|
||||
|
||||
export function createUsageProviderApi<Key extends UsageProviderApiKey>(
|
||||
ipc: Pick<IpcRenderer, 'invoke'>,
|
||||
ipc: {
|
||||
invoke(channel: string, args?: unknown, purpose?: BackendConnectionPurpose): Promise<unknown>
|
||||
onConnectionChanged?: (listener: BackendConnectionListener) => () => void
|
||||
},
|
||||
prefix: Key
|
||||
): PreloadApi[Key]
|
||||
export function createUsageProviderApi(
|
||||
ipc: Pick<IpcRenderer, 'invoke'>,
|
||||
ipc: {
|
||||
invoke(channel: string, args?: unknown, purpose?: BackendConnectionPurpose): Promise<unknown>
|
||||
onConnectionChanged?: (listener: BackendConnectionListener) => () => void
|
||||
},
|
||||
prefix: UsageProviderApiKey
|
||||
): UsageProviderApi {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The prefix selects its typed service methods; only the transport boundary erases their return types.
|
||||
return {
|
||||
getScanState: () => ipc.invoke(`${prefix}:getScanState`),
|
||||
...(ipc.onConnectionChanged ? { onConnectionChanged: ipc.onConnectionChanged } : {}),
|
||||
getScanState: (purpose?: BackendConnectionPurpose) =>
|
||||
purpose
|
||||
? ipc.invoke(`${prefix}:getScanState`, undefined, purpose)
|
||||
: ipc.invoke(`${prefix}:getScanState`),
|
||||
setEnabled: (args: { enabled: boolean }) => ipc.invoke(`${prefix}:setEnabled`, args),
|
||||
refresh: (args?: { force?: boolean }) => ipc.invoke(`${prefix}:refresh`, args),
|
||||
getSnapshot: (args: UsageRangeArgs & { limit?: number }) =>
|
||||
ipc.invoke(`${prefix}:getSnapshot`, args),
|
||||
getSnapshot: (args: UsageRangeArgs & { limit?: number }, purpose?: BackendConnectionPurpose) =>
|
||||
purpose
|
||||
? ipc.invoke(`${prefix}:getSnapshot`, args, purpose)
|
||||
: ipc.invoke(`${prefix}:getSnapshot`, args),
|
||||
getSummary: (args: UsageRangeArgs) => ipc.invoke(`${prefix}:getSummary`, args),
|
||||
getDaily: (args: UsageRangeArgs) => ipc.invoke(`${prefix}:getDaily`, args),
|
||||
getBreakdown: (args: UsageRangeArgs & { kind: string }) =>
|
||||
ipc.invoke(`${prefix}:getBreakdown`, args),
|
||||
getRecentSessions: (args: UsageRangeArgs & { limit?: number }) =>
|
||||
ipc.invoke(`${prefix}:getRecentSessions`, args)
|
||||
}
|
||||
} as UsageProviderApi
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user