Merge remote-tracking branch 'origin/master' into dshw/pr-2250

This commit is contained in:
_Kerman
2026-08-11 22:33:12 +08:00
422 files changed
+15607 -1120

No files matched your search

@@ -1,4 +1,4 @@
import type { IApiClient, HostFrame, MuxFrame, RpcRequest } from './api.ts'
import type { HostDescription, IApiClient, HostFrame, MuxFrame, RpcRequest } from './api.ts'
/** Reconnect/backoff tunables (deployment-varying — no hardcoded tunables; these become the
* future `ctx.connection` plugin's Config). All fields optional; defaults below. */
@@ -45,7 +45,7 @@ export interface ConnectionSinks {
onMuxEnvelope?: (envelope: RpcRequest<MuxFrame>) => void
onHostEnvelope?: (envelope: RpcRequest<HostFrame>) => void
/** After each connection generation is established (both streams open + describe succeeded), first connect included. */
onConnected?: () => void
onConnected?: (description: HostDescription) => void
/** Coarse state transitions (deduplicated: fires only on change). The initial pre-connect
* span reports nothing — the UI treats "no state yet" as connecting, not as an outage. */
onStateChange?: (state: ConnectionState) => void
@@ -99,6 +99,11 @@ export class ConnectionController {
return this.running
}
/** Re-read both mutable liveness guards after a potentially reentrant sink. */
private isGenerationActive(controller: AbortController): boolean {
return this.isRunning() && !controller.signal.aborted
}
private async loop(): Promise<void> {
while (this.running) {
const gen = ++this.generation
@@ -143,7 +148,11 @@ export class ConnectionController {
if (ac.signal.aborted) throw new Error('generation aborted during readiness handshake')
this.attempt = 0
this.emitState('connected')
this.callSink(this.sinks.onConnected)
// A state sink may synchronously stop this controller. Do not publish
// a description for a generation that no longer exists afterward.
if (this.isGenerationActive(ac)) {
this.callSink(() => { this.sinks.onConnected?.(descriptionResult.value) })
}
} catch {
// Transport failure: treat as generation failure, fall through to the shared backoff.
if (!ac.signal.aborted) ac.abort()
@@ -183,8 +192,7 @@ export class ConnectionController {
}
/** Sink exception isolation: a business-layer throw is logged only, never affecting pump or reconnect semantics. */
private callSink(fn: (() => void) | undefined): void {
if (fn === undefined) return
private callSink(fn: () => void): void {
try {
fn()
} catch (error) {
@@ -2305,7 +2305,9 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
interrupt: request => Promise.resolve(ok(request, { accepted: true as const })),
},
host: {
describe: request => ok(request, { version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions }),
describe: request => ok(request, {
version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions, canOpenPath: true,
}),
// Deterministic native pick: the keyless lanes drive the full
// pick-then-adopt path without an OS chooser (design-mock content,
// same tree the browse primitives serve).
+52 -3
View File
@@ -4,7 +4,7 @@
* controller with its sinks.
*/
import type { Context } from '@deepseek-ai/cordis'
import type { IApiClient } from './api.ts'
import type { HostDescription, IApiClient } from './api.ts'
import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts'
import { FixtureApiClient } from './fixture.ts'
import { WebApiClient } from './web-api-client.ts'
@@ -41,6 +41,13 @@ export {
export type { ConnectionConfig, ConnectionSinks, ConnectionState }
export type { ClientConnectionRpc } from '../rpc.ts'
/** Observable Host description published by each completed connection handshake. */
export interface HostDescriptionSource {
/** Latest connected-generation description; absent before connect and while reconnecting. */
getSnapshot(): HostDescription | undefined
/** Subscribe to description replacement and connection loss. */
subscribe(listener: () => void): () => void
}
/** Required services (none — this is the wire root). */
export const inject: string[] = []
@@ -55,6 +62,8 @@ export interface ConnectionHandle {
readonly api: IApiClient
/** Whether the current page authority is loopback; non-browser contexts default to true. */
readonly isLoopback: boolean
/** Generation-scoped Host facts, including native path-open capability. */
readonly hostDescription: HostDescriptionSource
/** Generic logical RPC channels over the same Connection transport. */
readonly rpc: ClientConnectionRpc
/**
@@ -79,16 +88,56 @@ export function apply(ctx: Context): void {
const api: IApiClient = fixtureClient ?? new WebApiClient()
const rpc = fixtureClient?.rpc ?? createWebConnectionRpc()
let started = false
let description: HostDescription | undefined
const descriptionListeners = new Set<() => void>()
const publishDescription = (next: HostDescription | undefined): void => {
if (Object.is(description, next)) return
description = next
for (const listener of [...descriptionListeners]) {
try {
listener()
} catch (error) {
console.error('[web-runtime] host-description listener threw:', error)
}
}
}
const handle: ConnectionHandle = {
api,
isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname),
hostDescription: {
getSnapshot: () => description,
subscribe: (listener) => {
descriptionListeners.add(listener)
return () => { descriptionListeners.delete(listener) }
},
},
rpc,
start(sinks, config) {
if (started) throw new Error('connection: the stream loop is already owned by another consumer')
started = true
const controller = new ConnectionController(api, sinks, config ?? {})
const controller = new ConnectionController(api, {
...sinks,
onConnected: (next) => {
publishDescription(next)
// A description subscriber may synchronously stop the loop. In that
// case publishDescription(undefined) has already retracted this
// generation, so do not leak its stale connected notification to
// the consumer sink afterward.
if (!Object.is(description, next)) return
sinks.onConnected?.(next)
},
onStateChange: (state) => {
if (state === 'reconnecting') publishDescription(undefined)
sinks.onStateChange?.(state)
},
}, config ?? {})
controller.start()
return { stop: () => { controller.stop() } }
return {
stop: () => {
controller.stop()
publishDescription(undefined)
},
}
},
}
ctx.provide('connection', handle)