perf: subscribe checks to repository revisions

This commit is contained in:
Neil
2026-07-30 05:57:56 -07:00
parent 6fbeb2abda
commit f705702782
28 changed files with 2289 additions and 138 deletions
+45 -82
View File
@@ -1,10 +1,8 @@
import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../shared/types'
import type { GitRepositorySnapshotRevisionEvent } from '../../shared/git-repository-snapshot'
import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe'
import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner'
import {
createGitRepositoryProjectionFreshness,
EMPTY_GIT_IGNORED_PATHS,
EMPTY_GIT_STATUS_ENTRIES,
freezeGitRepositoryStatus,
freezeGitRepositoryUpstream,
gitRepositoryScopeKey,
@@ -13,9 +11,13 @@ import {
type GitRepositoryExecutionIdentity,
type GitRepositorySnapshot,
type GitRepositorySnapshotQuery,
type GitRepositoryStatusIdentity,
type GitRepositoryStatusProjection
type GitRepositoryStatusIdentity
} from './git-repository-snapshot-projection'
import {
GitRepositorySnapshotRevisionSubscriptions,
type GitRepositoryRecord
} from './git-repository-snapshot-revision-subscriptions'
import { createGitRepositorySnapshot } from './git-repository-snapshot-reader'
export type {
GitRepositoryExecutionIdentity,
@@ -28,19 +30,6 @@ export type {
export const MAX_GIT_REPOSITORY_SNAPSHOTS = 64
export const MAX_GIT_REPOSITORY_PROJECTION_VARIANTS = 4
type ProjectionRecord<T> = {
generation: number
generatedAt: number
revision: number
value: T
failedGeneration?: number
}
type RepositoryRecord = {
status: Map<string, ProjectionRecord<GitRepositoryStatusProjection>>
upstream: Map<string, ProjectionRecord<Readonly<GitUpstreamStatus>>>
}
function clampBound(value: number, maximum: number): number {
return Number.isFinite(value) ? Math.min(maximum, Math.max(1, Math.floor(value))) : maximum
}
@@ -48,7 +37,8 @@ function clampBound(value: number, maximum: number): number {
export class GitRepositorySnapshotOwner {
private readonly statusReads = new GitStatusReadLeaseOwner<GitStatusResult>()
private readonly upstreamReads = new InFlightPromiseDedupe<GitUpstreamStatus>()
private readonly repositories = new Map<string, RepositoryRecord>()
private readonly repositories = new Map<string, GitRepositoryRecord>()
private readonly revisionSubscriptions = new GitRepositorySnapshotRevisionSubscriptions()
private readonly maxRepositories: number
private readonly maxProjectionVariants: number
private generation = 0
@@ -141,72 +131,31 @@ export class GitRepositorySnapshotOwner {
this.touchRepository(repositoryKey, repository)
const statusIdentity = gitRepositoryStatusKey(query.statusIdentity)
const upstreamIdentity = gitRepositoryUpstreamKey(query.pushTarget)
const statusRecord = repository.status.get(statusIdentity)
const upstreamRecord = repository.upstream.get(upstreamIdentity)
if (!statusRecord && !upstreamRecord) {
return null
}
const statusFreshness = createGitRepositoryProjectionFreshness(
statusRecord,
return createGitRepositorySnapshot(
repository,
this.generation,
statusIdentity
statusIdentity,
upstreamIdentity,
query.pushTarget === undefined
)
const embeddedUpstreamRecord =
query.pushTarget === undefined && statusRecord?.value.upstream ? statusRecord : undefined
const useEmbeddedUpstream =
embeddedUpstreamRecord &&
(!upstreamRecord || embeddedUpstreamRecord.revision > upstreamRecord.revision)
const selectedUpstreamRecord = useEmbeddedUpstream ? embeddedUpstreamRecord : upstreamRecord
const upstreamFreshness = selectedUpstreamRecord
? createGitRepositoryProjectionFreshness(
selectedUpstreamRecord,
this.generation,
useEmbeddedUpstream ? `status:${statusIdentity}` : upstreamIdentity
)
: createGitRepositoryProjectionFreshness(undefined, this.generation, upstreamIdentity)
const revision = Math.max(statusRecord?.revision ?? 0, upstreamRecord?.revision ?? 0)
const generatedAt = Math.max(statusRecord?.generatedAt ?? 0, upstreamRecord?.generatedAt ?? 0)
const status = statusRecord?.value
return Object.freeze({
revision,
generatedAt,
repositoryIdentity: status?.repositoryIdentity ?? Object.freeze({ head: null, branch: null }),
status:
status?.status ??
Object.freeze({
entries: EMPTY_GIT_STATUS_ENTRIES,
didHitLimit: false,
statusLength: null,
ignoredPaths: EMPTY_GIT_IGNORED_PATHS,
lineStatsState: 'missing' as const,
retentionTruncated: false
}),
upstream: useEmbeddedUpstream
? embeddedUpstreamRecord.value.upstream
: (upstreamRecord?.value ?? null),
conflicts: status?.conflicts ?? null,
worktreeGraphVersion: 0,
freshness: Object.freeze({
repositoryIdentity: statusFreshness,
status: statusFreshness,
upstream: upstreamFreshness,
conflicts: statusFreshness,
worktreeGraph: Object.freeze({
state: 'placeholder',
generation: this.generation,
currentGeneration: this.generation,
revision: null,
identity: null
})
})
})
}
subscribe(
query: GitRepositorySnapshotQuery,
listener: (event: GitRepositorySnapshotRevisionEvent) => void
): () => void {
return this.revisionSubscriptions.subscribe(query, listener)
}
invalidate(): void {
this.generation += 1
this.statusReads.invalidate()
this.upstreamReads.clear()
this.revisionSubscriptions.invalidate(this.generation, this.revision)
}
getSubscriptionCountForTests(): number {
return this.revisionSubscriptions.count
}
getRetentionState(): Readonly<{
@@ -227,7 +176,7 @@ export class GitRepositorySnapshotOwner {
})
}
private getOrCreateRepository(repositoryKey: string): RepositoryRecord {
private getOrCreateRepository(repositoryKey: string): GitRepositoryRecord {
const existing = this.repositories.get(repositoryKey)
if (existing) {
this.touchRepository(repositoryKey, existing)
@@ -245,12 +194,12 @@ export class GitRepositorySnapshotOwner {
return repository
}
private touchRepository(repositoryKey: string, repository: RepositoryRecord): void {
private touchRepository(repositoryKey: string, repository: GitRepositoryRecord): void {
this.repositories.delete(repositoryKey)
this.repositories.set(repositoryKey, repository)
}
private removeEmptyRepository(repositoryKey: string, repository: RepositoryRecord): void {
private removeEmptyRepository(repositoryKey: string, repository: GitRepositoryRecord): void {
if (
this.repositories.get(repositoryKey) === repository &&
repository.status.size === 0 &&
@@ -262,7 +211,7 @@ export class GitRepositorySnapshotOwner {
private publishStatus(
repositoryKey: string,
repository: RepositoryRecord,
repository: GitRepositoryRecord,
projectionKey: string,
generation: number,
result: GitStatusResult
@@ -270,6 +219,7 @@ export class GitRepositorySnapshotOwner {
if (generation !== this.generation || this.repositories.get(repositoryKey) !== repository) {
return
}
const previous = repository.status.get(projectionKey)
const revision = this.nextRevision()
const generatedAt = this.now()
this.remember(repository.status, projectionKey, {
@@ -278,11 +228,18 @@ export class GitRepositorySnapshotOwner {
revision,
value: freezeGitRepositoryStatus(result)
})
this.revisionSubscriptions.statusPublished(
repositoryKey,
repository,
projectionKey,
previous,
this.generation
)
}
private publishUpstream(
repositoryKey: string,
repository: RepositoryRecord,
repository: GitRepositoryRecord,
projectionKey: string,
generation: number,
result: GitUpstreamStatus
@@ -296,6 +253,12 @@ export class GitRepositorySnapshotOwner {
revision: this.nextRevision(),
value: freezeGitRepositoryUpstream(result)
})
this.revisionSubscriptions.upstreamPublished(
repositoryKey,
repository,
projectionKey,
this.generation
)
}
private remember<T>(projections: Map<string, T>, key: string, value: T): void {
@@ -0,0 +1,76 @@
import type { GitRepositorySnapshot } from '../../shared/git-repository-snapshot'
import {
createGitRepositoryProjectionFreshness,
EMPTY_GIT_IGNORED_PATHS,
EMPTY_GIT_STATUS_ENTRIES
} from './git-repository-snapshot-projection'
import type { GitRepositoryRecord } from './git-repository-snapshot-revision-subscriptions'
export function createGitRepositorySnapshot(
repository: GitRepositoryRecord,
generation: number,
statusIdentity: string,
upstreamIdentity: string,
usesConfiguredUpstream: boolean
): GitRepositorySnapshot | null {
const statusRecord = repository.status.get(statusIdentity)
const upstreamRecord = repository.upstream.get(upstreamIdentity)
if (!statusRecord && !upstreamRecord) {
return null
}
const statusFreshness = createGitRepositoryProjectionFreshness(
statusRecord,
generation,
statusIdentity
)
const embeddedUpstreamRecord =
usesConfiguredUpstream && statusRecord?.value.upstream ? statusRecord : undefined
const useEmbeddedUpstream =
embeddedUpstreamRecord &&
(!upstreamRecord || embeddedUpstreamRecord.revision > upstreamRecord.revision)
const selectedUpstreamRecord = useEmbeddedUpstream ? embeddedUpstreamRecord : upstreamRecord
const upstreamFreshness = selectedUpstreamRecord
? createGitRepositoryProjectionFreshness(
selectedUpstreamRecord,
generation,
useEmbeddedUpstream ? `status:${statusIdentity}` : upstreamIdentity
)
: createGitRepositoryProjectionFreshness(undefined, generation, upstreamIdentity)
const revision = Math.max(statusRecord?.revision ?? 0, upstreamRecord?.revision ?? 0)
const generatedAt = Math.max(statusRecord?.generatedAt ?? 0, upstreamRecord?.generatedAt ?? 0)
const status = statusRecord?.value
return Object.freeze({
revision,
generatedAt,
repositoryIdentity: status?.repositoryIdentity ?? Object.freeze({ head: null, branch: null }),
status:
status?.status ??
Object.freeze({
entries: EMPTY_GIT_STATUS_ENTRIES,
didHitLimit: false,
statusLength: null,
ignoredPaths: EMPTY_GIT_IGNORED_PATHS,
lineStatsState: 'missing' as const,
retentionTruncated: false
}),
upstream: useEmbeddedUpstream
? embeddedUpstreamRecord.value.upstream
: (upstreamRecord?.value ?? null),
conflicts: status?.conflicts ?? null,
worktreeGraphVersion: 0,
freshness: Object.freeze({
repositoryIdentity: statusFreshness,
status: statusFreshness,
upstream: upstreamFreshness,
conflicts: statusFreshness,
worktreeGraph: Object.freeze({
state: 'placeholder',
generation,
currentGeneration: generation,
revision: null,
identity: null
})
})
})
}
@@ -0,0 +1,198 @@
import type { GitRepositorySnapshotRevisionEvent } from '../../shared/git-repository-snapshot'
import type { GitUpstreamStatus } from '../../shared/types'
import type { GitRepositoryStatusProjection } from './git-repository-snapshot-projection'
import {
gitRepositoryScopeKey,
gitRepositoryStatusKey,
gitRepositoryUpstreamKey,
type GitRepositorySnapshotQuery
} from './git-repository-snapshot-projection'
export type GitRepositoryProjectionRecord<T> = {
generation: number
generatedAt: number
revision: number
value: T
failedGeneration?: number
}
export type GitRepositoryRecord = {
status: Map<string, GitRepositoryProjectionRecord<GitRepositoryStatusProjection>>
upstream: Map<string, GitRepositoryProjectionRecord<Readonly<GitUpstreamStatus>>>
}
type SnapshotSubscription = {
statusKey: string
upstreamKey: string
usesConfiguredUpstream: boolean
lastReadyRevision: number
listener: (event: GitRepositorySnapshotRevisionEvent) => void
}
export class GitRepositorySnapshotRevisionSubscriptions {
private readonly subscriptions = new Map<string, Set<SnapshotSubscription>>()
subscribe(
query: GitRepositorySnapshotQuery,
listener: (event: GitRepositorySnapshotRevisionEvent) => void
): () => void {
const repositoryKey = gitRepositoryScopeKey(query.executionIdentity, query.worktreePath)
const subscription: SnapshotSubscription = {
statusKey: gitRepositoryStatusKey(query.statusIdentity),
upstreamKey: gitRepositoryUpstreamKey(query.pushTarget),
usesConfiguredUpstream: query.pushTarget === undefined,
lastReadyRevision: 0,
listener
}
const subscriptions = this.subscriptions.get(repositoryKey) ?? new Set()
subscriptions.add(subscription)
this.subscriptions.set(repositoryKey, subscriptions)
return () => {
subscriptions.delete(subscription)
if (subscriptions.size === 0) {
this.subscriptions.delete(repositoryKey)
}
}
}
invalidate(generation: number, revision: number): void {
const event = Object.freeze({ state: 'invalidated' as const, generation, revision })
for (const subscriptions of this.subscriptions.values()) {
for (const subscription of subscriptions) {
subscription.listener(event)
}
}
}
statusPublished(
repositoryKey: string,
repository: GitRepositoryRecord,
projectionKey: string,
previous: GitRepositoryProjectionRecord<GitRepositoryStatusProjection> | undefined,
generation: number
): void {
const subscriptions = this.subscriptions.get(repositoryKey)
const status = repository.status.get(projectionKey)
if (!subscriptions || !status || !this.statusRecordIsReady(status, generation)) {
return
}
const events = new Map<number, GitRepositorySnapshotRevisionEvent>()
for (const subscription of subscriptions) {
if (subscription.statusKey !== projectionKey) {
continue
}
if (subscription.usesConfiguredUpstream) {
const embedded = status.value.upstream
if (embedded) {
if (this.upstreamValueIsComplete(embedded)) {
this.emitReady(subscription, generation, status.revision, events)
}
continue
}
}
const upstream = repository.upstream.get(subscription.upstreamKey)
if (
!this.upstreamRecordIsReady(upstream, generation) ||
!this.upstreamValueIsComplete(upstream.value) ||
(upstream.revision <= status.revision &&
!this.repositoryIdentityMatches(previous?.value, status.value))
) {
continue
}
this.emitReady(subscription, generation, Math.max(status.revision, upstream.revision), events)
}
}
upstreamPublished(
repositoryKey: string,
repository: GitRepositoryRecord,
projectionKey: string,
generation: number
): void {
const subscriptions = this.subscriptions.get(repositoryKey)
const upstream = repository.upstream.get(projectionKey)
if (
!subscriptions ||
!this.upstreamRecordIsReady(upstream, generation) ||
!this.upstreamValueIsComplete(upstream.value)
) {
return
}
const events = new Map<number, GitRepositorySnapshotRevisionEvent>()
for (const subscription of subscriptions) {
if (subscription.upstreamKey !== projectionKey) {
continue
}
const status = repository.status.get(subscription.statusKey)
if (!status || !this.statusRecordIsReady(status, generation)) {
continue
}
this.emitReady(subscription, generation, Math.max(status.revision, upstream.revision), events)
}
}
get count(): number {
let count = 0
for (const subscriptions of this.subscriptions.values()) {
count += subscriptions.size
}
return count
}
private statusRecordIsReady(
record: GitRepositoryProjectionRecord<GitRepositoryStatusProjection>,
generation: number
): boolean {
return (
record.generation === generation &&
record.failedGeneration !== generation &&
!record.value.status.retentionTruncated
)
}
private upstreamRecordIsReady(
record: GitRepositoryProjectionRecord<Readonly<GitUpstreamStatus>> | undefined,
generation: number
): record is GitRepositoryProjectionRecord<Readonly<GitUpstreamStatus>> {
return (
record !== undefined &&
record.generation === generation &&
record.failedGeneration !== generation
)
}
private upstreamValueIsComplete(value: Readonly<GitUpstreamStatus>): boolean {
return !(
value.ahead > 0 &&
value.behind > 0 &&
value.behindCommitsArePatchEquivalent === undefined
)
}
private repositoryIdentityMatches(
previous: GitRepositoryStatusProjection | undefined,
current: GitRepositoryStatusProjection
): boolean {
return (
previous !== undefined &&
previous.repositoryIdentity.head === current.repositoryIdentity.head &&
previous.repositoryIdentity.branch === current.repositoryIdentity.branch
)
}
private emitReady(
subscription: SnapshotSubscription,
generation: number,
revision: number,
events: Map<number, GitRepositorySnapshotRevisionEvent>
): void {
if (revision <= subscription.lastReadyRevision) {
return
}
subscription.lastReadyRevision = revision
const event =
events.get(revision) ?? Object.freeze({ state: 'ready' as const, generation, revision })
events.set(revision, event)
subscription.listener(event)
}
}
@@ -0,0 +1,316 @@
import { describe, expect, it, vi } from 'vitest'
import type { GitStatusResult } from '../../shared/types'
import type { GitRepositorySnapshotRevisionEvent } from '../../shared/git-repository-snapshot'
import {
GitRepositorySnapshotOwner,
type GitRepositoryExecutionIdentity,
type GitRepositoryStatusIdentity
} from './git-repository-snapshot-owner'
const native = { kind: 'native' } as const
const defaultStatusIdentity: GitRepositoryStatusIdentity = {
includeIgnored: false,
reuseLineStats: false,
bypassEffectiveUpstreamNegativeCache: false,
limit: 1_000,
sharedLinkPaths: []
}
const statusResult: GitStatusResult = {
entries: [],
conflictOperation: 'unknown',
head: 'abc123',
branch: 'refs/heads/main',
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 1,
behind: 0
}
}
const explicitTarget = {
remoteName: 'fork',
branchName: 'feature',
remoteUrl: 'ssh://git.example/repo',
remoteCreated: false
}
function query(
executionIdentity: GitRepositoryExecutionIdentity = native,
worktreePath = '/repo',
statusIdentity: GitRepositoryStatusIdentity = defaultStatusIdentity,
pushTarget = explicitTarget
) {
return { executionIdentity, worktreePath, statusIdentity, pushTarget }
}
describe('GitRepositorySnapshotOwner revision subscriptions', () => {
it('publishes configured-upstream readiness without a second upstream read', async () => {
const owner = new GitRepositorySnapshotOwner()
const events: GitRepositorySnapshotRevisionEvent[] = []
owner.subscribe({ ...query(), pushTarget: undefined }, (event) => events.push(event))
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
expect(events).toEqual([{ state: 'ready', generation: 0, revision: 1 }])
})
it('waits for a complete explicit upstream projection before publishing readiness', async () => {
const owner = new GitRepositorySnapshotOwner()
const events: GitRepositorySnapshotRevisionEvent[] = []
owner.subscribe(query(), (event) => events.push(event))
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
expect(events).toEqual([])
await owner.readUpstream(native, '/repo', explicitTarget, async () => ({
hasUpstream: true,
upstreamName: 'fork/feature',
ahead: 1,
behind: 1
}))
expect(events).toEqual([])
await owner.readUpstream(native, '/repo', explicitTarget, async () => ({
hasUpstream: true,
upstreamName: 'fork/feature',
ahead: 1,
behind: 1,
behindCommitsArePatchEquivalent: false
}))
expect(events).toEqual([{ state: 'ready', generation: 0, revision: 3 }])
})
it('does not admit an ambiguous embedded upstream through an older complete projection', async () => {
const owner = new GitRepositorySnapshotOwner()
const events: GitRepositorySnapshotRevisionEvent[] = []
const configuredQuery = { ...query(), pushTarget: undefined }
owner.subscribe(configuredQuery, (event) => events.push(event))
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
await owner.readUpstream(native, '/repo', undefined, async () => ({
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 1,
behind: 0
}))
events.length = 0
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => ({
...statusResult,
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 1,
behind: 1
}
}))
expect(events).toEqual([])
expect(owner.getSnapshot(configuredQuery)?.upstream).toEqual({
hasUpstream: true,
upstreamName: 'origin/main',
ahead: 1,
behind: 1
})
})
it('keeps overlapping producer and revision-driven consumer demand at one physical read each', async () => {
const owner = new GitRepositorySnapshotOwner()
const statusLoad = vi.fn(async () => statusResult)
const upstreamLoad = vi.fn(async () => ({
hasUpstream: true,
upstreamName: 'fork/feature',
ahead: 0,
behind: 0
}))
const snapshots: unknown[] = []
owner.subscribe(query(), (event) => {
if (event.state === 'ready') {
snapshots.push(owner.getSnapshot(query()))
}
})
await Promise.all([
owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad),
owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, statusLoad)
])
await Promise.all([
owner.readUpstream(native, '/repo', explicitTarget, upstreamLoad),
owner.readUpstream(native, '/repo', explicitTarget, upstreamLoad)
])
expect(statusLoad).toHaveBeenCalledOnce()
expect(upstreamLoad).toHaveBeenCalledOnce()
expect(snapshots).toHaveLength(1)
})
it('reuses a retained explicit upstream only while repository identity is unchanged', async () => {
const owner = new GitRepositorySnapshotOwner()
const events: GitRepositorySnapshotRevisionEvent[] = []
owner.subscribe(query(), (event) => events.push(event))
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
await owner.readUpstream(native, '/repo', explicitTarget, async () => ({
hasUpstream: true,
upstreamName: 'fork/feature',
ahead: 0,
behind: 0
}))
events.length = 0
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => ({
...statusResult,
entries: [{ path: 'src/app.ts', status: 'modified', area: 'unstaged' }]
}))
expect(events).toEqual([{ state: 'ready', generation: 0, revision: 3 }])
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => ({
...statusResult,
head: 'def456',
branch: 'refs/heads/other'
}))
expect(events).toHaveLength(1)
await owner.readUpstream(native, '/repo', explicitTarget, async () => ({
hasUpstream: true,
upstreamName: 'fork/feature',
ahead: 2,
behind: 0
}))
expect(events.at(-1)).toEqual({ state: 'ready', generation: 0, revision: 5 })
})
it('reports both mutation fences and suppresses stale completion readiness', async () => {
const owner = new GitRepositorySnapshotOwner()
const events: GitRepositorySnapshotRevisionEvent[] = []
let resolveStatus!: (result: GitStatusResult) => void
const pending = new Promise<GitStatusResult>((resolve) => {
resolveStatus = resolve
})
owner.subscribe({ ...query(), pushTarget: undefined }, (event) => events.push(event))
const stale = owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, () => pending)
owner.invalidate()
resolveStatus(statusResult)
await stale
owner.invalidate()
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
expect(events).toEqual([
{ state: 'invalidated', generation: 1, revision: 0 },
{ state: 'invalidated', generation: 2, revision: 0 },
{ state: 'ready', generation: 2, revision: 1 }
])
})
it('isolates exact host, path, status-option, and target subscriptions', async () => {
const owner = new GitRepositorySnapshotOwner()
const matching = vi.fn()
const listeners = [
owner.subscribe(query(), matching),
owner.subscribe(query({ kind: 'wsl', distro: 'Ubuntu' }), vi.fn()),
owner.subscribe(query(native, '/other'), vi.fn()),
owner.subscribe(
query(native, '/repo', { ...defaultStatusIdentity, reuseLineStats: true }),
vi.fn()
),
owner.subscribe(
query(native, '/repo', defaultStatusIdentity, { ...explicitTarget, remoteCreated: true }),
vi.fn()
)
]
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
await owner.readUpstream(native, '/repo', explicitTarget, async () => ({
hasUpstream: false,
ahead: 0,
behind: 0
}))
expect(matching).toHaveBeenCalledOnce()
for (const unsubscribe of listeners.slice(1)) {
expect(unsubscribe).toEqual(expect.any(Function))
}
})
it('releases exact listeners and shares one immutable event allocation across fan-out', async () => {
const owner = new GitRepositorySnapshotOwner()
const events: GitRepositorySnapshotRevisionEvent[] = []
const unsubscribes = Array.from({ length: 100 }, () =>
owner.subscribe({ ...query(), pushTarget: undefined }, (event) => events.push(event))
)
await owner.readStatus(
native,
'/repo',
defaultStatusIdentity,
undefined,
async () => statusResult
)
expect(events).toHaveLength(100)
expect(new Set(events)).toHaveLength(1)
expect(Object.isFrozen(events[0])).toBe(true)
expect(owner.getSubscriptionCountForTests()).toBe(100)
for (const unsubscribe of unsubscribes) {
unsubscribe()
}
expect(owner.getSubscriptionCountForTests()).toBe(0)
})
it('does not publish failed or retention-truncated status projections', async () => {
const owner = new GitRepositorySnapshotOwner()
const listener = vi.fn()
owner.subscribe({ ...query(), pushTarget: undefined }, listener)
await expect(
owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => {
throw new Error('status failed')
})
).rejects.toThrow('status failed')
await owner.readStatus(native, '/repo', defaultStatusIdentity, undefined, async () => ({
...statusResult,
entries: Array.from({ length: 1_001 }, (_, index) => ({
path: `src/file-${index}.ts`,
status: 'modified' as const,
area: 'unstaged' as const
}))
}))
expect(listener).not.toHaveBeenCalled()
})
})
+18
View File
@@ -54,6 +54,7 @@ import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight
import type { GitRuntimeOptions } from './git-runtime-options'
import { gitOptionsForWorktree } from './git-runtime-options'
import { parseGitRevListFirstParentOid } from '../../shared/git-rev-list-output'
import type { GitRepositorySnapshotRevisionEvent } from '../../shared/git-repository-snapshot'
import {
nativeAndWslGitRepositorySnapshotOwner,
type GitRepositoryExecutionIdentity,
@@ -243,6 +244,23 @@ export function getGitRepositorySnapshot(
})
}
export function subscribeGitRepositorySnapshot(
worktreePath: string,
options: GetStatusOptions,
pushTarget: GitPushTarget | undefined,
listener: (event: GitRepositorySnapshotRevisionEvent) => void
): () => void {
return nativeAndWslGitRepositorySnapshotOwner.subscribe(
{
executionIdentity: getStatusExecutionIdentity(options),
worktreePath,
statusIdentity: getStatusSnapshotIdentity(options),
pushTarget
},
listener
)
}
function getStatusExecutionIdentity(options: GetStatusOptions): GitRepositoryExecutionIdentity {
return options.wslDistro ? { kind: 'wsl', distro: options.wslDistro } : { kind: 'native' }
}
+8 -25
View File
@@ -29,7 +29,10 @@ import type {
TuiAgent
} from '../../shared/types'
import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history'
import type { GitRepositorySnapshot } from '../../shared/git-repository-snapshot'
import type {
GitRepositorySnapshot,
GitRepositorySnapshotRequest
} from '../../shared/git-repository-snapshot'
import type { SshMutationExpectation } from '../../shared/ssh-types'
import { assertSshMutationExpectation } from '../ssh/ssh-connection-generation'
import {
@@ -136,6 +139,7 @@ import { sanitizeLocalDownloadFilename } from '../local-download-filename'
import { registerFilesystemDownloadFolderHandlers } from './filesystem-download-folder'
import { getWorktreeSharedLinkPaths } from '../git/worktree-shared-directories'
import { createSenderScopedRequestCancellations } from './sender-scoped-request-cancellation'
import { resolveLocalGitRepositorySnapshotRequest } from './git-repository-snapshot-request'
// Why: Monaco degrades features on large files like VS Code, so a 5MB block would needlessly lock out ordinary JSON/log files.
const MAX_TEXT_FILE_SIZE = 50 * 1024 * 1024 // 50MB
@@ -1145,17 +1149,7 @@ export function registerFilesystemHandlers(
ipcMain.handle(
'git:repositorySnapshot',
async (
_event,
args: {
worktreePath: string
connectionId?: string
includeIgnored?: boolean
bypassEffectiveUpstreamNegativeCache?: boolean
reuseLineStats?: boolean
pushTarget?: GitPushTarget
}
): Promise<GitRepositorySnapshot | null> => {
async (_event, args: GitRepositorySnapshotRequest): Promise<GitRepositorySnapshot | null> => {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
}
@@ -1173,19 +1167,8 @@ export function registerFilesystemHandlers(
}
return provider.getRepositorySnapshot(args.worktreePath, options, args.pushTarget)
}
const worktreePath = await resolveRegisteredWorktreePath(args.worktreePath, store)
const repo = getLocalRepoForRegisteredWorktree(store, args.worktreePath, worktreePath)
const gitOptions = getLocalGitOptionsForRepo(store, repo)
const sharedLinkPaths = repo ? getWorktreeSharedLinkPaths(repo) : []
return getGitRepositorySnapshot(
worktreePath,
{
...options,
...gitOptions,
...(sharedLinkPaths.length > 0 ? { sharedLinkPaths } : {})
},
args.pushTarget
)
const request = await resolveLocalGitRepositorySnapshotRequest(store, args)
return getGitRepositorySnapshot(request.worktreePath, request.options, args.pushTarget)
}
)
@@ -0,0 +1,30 @@
import type { GitRepositorySnapshotRequest } from '../../shared/git-repository-snapshot'
import type { Store } from '../persistence'
import type { GetStatusOptions } from '../git/status'
import { getWorktreeSharedLinkPaths } from '../git/worktree-shared-directories'
import { resolveRegisteredWorktreePath } from './filesystem-auth'
import {
getLocalGitOptionsForRepo,
getLocalRepoForRegisteredWorktree
} from './local-worktree-runtime-options'
export async function resolveLocalGitRepositorySnapshotRequest(
store: Store,
args: GitRepositorySnapshotRequest
): Promise<{ worktreePath: string; options: GetStatusOptions }> {
const worktreePath = await resolveRegisteredWorktreePath(args.worktreePath, store)
const repo = getLocalRepoForRegisteredWorktree(store, args.worktreePath, worktreePath)
const sharedLinkPaths = repo ? getWorktreeSharedLinkPaths(repo) : []
return {
worktreePath,
options: {
includeIgnored: args.includeIgnored ?? false,
...(args.reuseLineStats === true ? { reuseLineStats: true } : {}),
...(args.bypassEffectiveUpstreamNegativeCache === true
? { bypassEffectiveUpstreamNegativeCache: true }
: {}),
...getLocalGitOptionsForRepo(store, repo),
...(sharedLinkPaths.length > 0 ? { sharedLinkPaths } : {})
}
}
}
@@ -0,0 +1,301 @@
import { EventEmitter } from 'node:events'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
handlers: new Map<string, (event: never, args: never) => unknown>(),
localSubscribe: vi.fn(),
registrySubscribe: vi.fn(),
resolveLocal: vi.fn(),
getProvider: vi.fn(),
getGeneration: vi.fn()
}))
vi.mock('electron', () => ({
ipcMain: {
handle: (channel: string, handler: (event: never, args: never) => unknown) => {
mocks.handlers.set(channel, handler)
},
removeHandler: (channel: string) => {
mocks.handlers.delete(channel)
}
}
}))
vi.mock('../git/status', () => ({
subscribeGitRepositorySnapshot: mocks.localSubscribe
}))
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: mocks.getProvider,
getSshGitProviderGeneration: mocks.getGeneration,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'unavailable',
subscribeSshGitProviderRegistry: mocks.registrySubscribe
}))
vi.mock('./git-repository-snapshot-request', () => ({
resolveLocalGitRepositorySnapshotRequest: mocks.resolveLocal
}))
import {
disposeGitRepositorySnapshotSubscriptionHandlers,
getGitRepositorySnapshotSubscriptionCountForTests,
registerGitRepositorySnapshotSubscriptionHandlers
} from './git-repository-snapshot-subscriptions'
type Sender = EventEmitter & {
id: number
isDestroyed: () => boolean
send: ReturnType<typeof vi.fn>
}
function sender(id: number): Sender {
const value = new EventEmitter() as Sender
value.id = id
value.isDestroyed = () => false
value.send = vi.fn()
return value
}
function handler(channel: string): (event: never, args: never) => Promise<unknown> {
return mocks.handlers.get(channel) as (event: never, args: never) => Promise<unknown>
}
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
let resolve!: (value: T) => void
const promise = new Promise<T>((complete) => {
resolve = complete
})
return { promise, resolve }
}
describe('Git repository snapshot subscription IPC', () => {
beforeEach(() => {
disposeGitRepositorySnapshotSubscriptionHandlers()
mocks.handlers.clear()
mocks.localSubscribe.mockReset()
mocks.localSubscribe.mockReturnValue(vi.fn())
mocks.registrySubscribe.mockReset()
mocks.registrySubscribe.mockReturnValue(vi.fn())
mocks.resolveLocal.mockReset()
mocks.resolveLocal.mockResolvedValue({
worktreePath: '/resolved/repo',
options: { includeIgnored: false, sharedLinkPaths: ['/shared'] }
})
mocks.getProvider.mockReset()
mocks.getGeneration.mockReset()
})
it('isolates senders and removes exact listeners on unsubscribe and destruction', async () => {
const ownerListeners: ((event: {
state: 'ready'
generation: number
revision: number
}) => void)[] = []
const ownerUnsubscribes = [vi.fn(), vi.fn()]
mocks.localSubscribe
.mockImplementationOnce(
(_path, _options, _target, listener: (typeof ownerListeners)[number]) => {
ownerListeners.push(listener)
return ownerUnsubscribes[0]
}
)
.mockImplementationOnce(
(_path, _options, _target, listener: (typeof ownerListeners)[number]) => {
ownerListeners.push(listener)
return ownerUnsubscribes[1]
}
)
registerGitRepositorySnapshotSubscriptionHandlers({} as never)
const first = sender(1)
const second = sender(2)
await handler('git:subscribeRepositorySnapshot')(
{ sender: first } as never,
{ subscriptionId: 'one', worktreePath: '/repo' } as never
)
await handler('git:subscribeRepositorySnapshot')(
{ sender: second } as never,
{ subscriptionId: 'two', worktreePath: '/repo' } as never
)
expect(mocks.localSubscribe).toHaveBeenNthCalledWith(
1,
'/resolved/repo',
{ includeIgnored: false, sharedLinkPaths: ['/shared'] },
undefined,
expect.any(Function)
)
ownerListeners[0]({ state: 'ready', generation: 2, revision: 3 })
expect(first.send).toHaveBeenCalledWith('git:repositorySnapshotRevision', {
subscriptionId: 'one',
event: { state: 'ready', generation: 2, revision: 3, incarnation: 0 }
})
expect(second.send).not.toHaveBeenCalled()
expect(first.send.mock.calls[0][1]).not.toHaveProperty('worktreePath')
await handler('git:unsubscribeRepositorySnapshot')(
{ sender: second } as never,
{ subscriptionId: 'one' } as never
)
expect(ownerUnsubscribes[0]).not.toHaveBeenCalled()
await handler('git:unsubscribeRepositorySnapshot')(
{ sender: first } as never,
{ subscriptionId: 'one' } as never
)
expect(ownerUnsubscribes[0]).toHaveBeenCalledTimes(1)
expect(first.listenerCount('destroyed')).toBe(0)
second.emit('destroyed')
expect(ownerUnsubscribes[1]).toHaveBeenCalledTimes(1)
expect(getGitRepositorySnapshotSubscriptionCountForTests()).toBe(0)
})
it('rebinds provider incarnations and suppresses late old-provider events', async () => {
let registryListener!: (event: {
connectionId: string
generation: number
provider: never
}) => void
mocks.registrySubscribe.mockImplementation((listener) => {
registryListener = listener
return vi.fn()
})
const firstListener = vi.fn()
const secondListener = vi.fn()
const firstUnsubscribe = vi.fn()
const secondUnsubscribe = vi.fn()
const firstProvider = {
subscribeRepositorySnapshot: vi.fn((_path, _options, _target, listener) => {
firstListener.mockImplementation(listener)
return firstUnsubscribe
})
}
const secondProvider = {
subscribeRepositorySnapshot: vi.fn((_path, _options, _target, listener) => {
secondListener.mockImplementation(listener)
return secondUnsubscribe
})
}
mocks.getProvider.mockReturnValue(firstProvider)
mocks.getGeneration.mockReturnValue(5)
registerGitRepositorySnapshotSubscriptionHandlers({} as never)
const target = sender(3)
await handler('git:subscribeRepositorySnapshot')(
{ sender: target } as never,
{
subscriptionId: 'ssh',
worktreePath: '/repo',
connectionId: 'connection',
reuseLineStats: true
} as never
)
firstListener({ state: 'ready', generation: 1, revision: 4 })
registryListener({
connectionId: 'connection',
generation: 6,
provider: secondProvider as never
})
firstListener({ state: 'ready', generation: 1, revision: 5 })
secondListener({ state: 'ready', generation: 0, revision: 1 })
expect(firstUnsubscribe).toHaveBeenCalledTimes(1)
expect(target.send.mock.calls.map((call) => call[1].event)).toEqual([
{ state: 'ready', generation: 1, revision: 4, incarnation: 5 },
{ state: 'invalidated', generation: 0, revision: 0, incarnation: 6 },
{ state: 'ready', generation: 0, revision: 1, incarnation: 6 }
])
disposeGitRepositorySnapshotSubscriptionHandlers()
expect(secondUnsubscribe).toHaveBeenCalledTimes(1)
})
it('does not bind an owner after sender destruction during local request resolution', async () => {
const resolution = deferred<{
worktreePath: string
options: { includeIgnored: boolean }
}>()
mocks.resolveLocal.mockReturnValue(resolution.promise)
registerGitRepositorySnapshotSubscriptionHandlers({} as never)
const target = sender(4)
let destroyed = false
target.isDestroyed = () => destroyed
const pending = handler('git:subscribeRepositorySnapshot')(
{ sender: target } as never,
{ subscriptionId: 'pending', worktreePath: '/repo' } as never
)
destroyed = true
target.emit('destroyed')
resolution.resolve({ worktreePath: '/repo', options: { includeIgnored: false } })
await pending
expect(mocks.localSubscribe).not.toHaveBeenCalled()
expect(getGitRepositorySnapshotSubscriptionCountForTests()).toBe(0)
})
it('closes a subscription when its renderer can no longer receive events', async () => {
let ownerListener!: (event: { state: 'ready'; generation: number; revision: number }) => void
const unsubscribe = vi.fn()
mocks.localSubscribe.mockImplementation((_path, _options, _target, listener) => {
ownerListener = listener
return unsubscribe
})
registerGitRepositorySnapshotSubscriptionHandlers({} as never)
const target = sender(5)
target.send.mockImplementation(() => {
throw new Error('render frame disposed')
})
await handler('git:subscribeRepositorySnapshot')(
{ sender: target } as never,
{ subscriptionId: 'send-failure', worktreePath: '/repo' } as never
)
expect(() => ownerListener({ state: 'ready', generation: 1, revision: 1 })).not.toThrow()
expect(unsubscribe).toHaveBeenCalledOnce()
expect(getGitRepositorySnapshotSubscriptionCountForTests()).toBe(0)
})
it('does not rebind SSH after replacement invalidation delivery fails', async () => {
let registryListener!: (event: {
connectionId: string
generation: number
provider: never
}) => void
mocks.registrySubscribe.mockImplementation((listener) => {
registryListener = listener
return vi.fn()
})
const firstUnsubscribe = vi.fn()
const firstProvider = {
subscribeRepositorySnapshot: vi.fn(() => firstUnsubscribe)
}
const secondProvider = {
subscribeRepositorySnapshot: vi.fn(() => vi.fn())
}
mocks.getProvider.mockReturnValue(firstProvider)
mocks.getGeneration.mockReturnValue(1)
registerGitRepositorySnapshotSubscriptionHandlers({} as never)
const target = sender(6)
await handler('git:subscribeRepositorySnapshot')(
{ sender: target } as never,
{
subscriptionId: 'ssh-send-failure',
worktreePath: '/repo',
connectionId: 'connection'
} as never
)
target.send.mockImplementation(() => {
throw new Error('render frame disposed')
})
registryListener({
connectionId: 'connection',
generation: 2,
provider: secondProvider as never
})
expect(firstUnsubscribe).toHaveBeenCalledOnce()
expect(secondProvider.subscribeRepositorySnapshot).not.toHaveBeenCalled()
expect(getGitRepositorySnapshotSubscriptionCountForTests()).toBe(0)
})
})
@@ -0,0 +1,218 @@
import { randomUUID } from 'node:crypto'
import { ipcMain } from 'electron'
import type { WebContents } from 'electron'
import type {
GitRepositorySnapshotRequest,
GitRepositorySnapshotRevisionEvent,
GitRepositorySnapshotSubscriptionEvent
} from '../../shared/git-repository-snapshot'
import { assertGitPushTargetShape } from '../../shared/git-push-target-validation'
import { subscribeGitRepositorySnapshot } from '../git/status'
import type { Store } from '../persistence'
import type { SshGitProvider } from '../providers/ssh-git-provider'
import {
getSshGitProvider,
getSshGitProviderGeneration,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE,
subscribeSshGitProviderRegistry
} from '../providers/ssh-git-dispatch'
import { resolveLocalGitRepositorySnapshotRequest } from './git-repository-snapshot-request'
const SUBSCRIBE_CHANNEL = 'git:subscribeRepositorySnapshot'
const UNSUBSCRIBE_CHANNEL = 'git:unsubscribeRepositorySnapshot'
export const GIT_REPOSITORY_SNAPSHOT_REVISION_CHANNEL = 'git:repositorySnapshotRevision'
type RetainedSubscription = {
id: string
args: GitRepositorySnapshotRequest
sender: WebContents
senderId: number
incarnation: number
bindingToken: number
unsubscribeOwner: () => void
removeDestroyedListener: () => void
}
const subscriptions = new Map<string, RetainedSubscription>()
let disposeRegistryListener: (() => void) | null = null
function closeSubscription(subscription: RetainedSubscription): void {
if (subscriptions.get(subscription.id) !== subscription) {
return
}
subscriptions.delete(subscription.id)
subscription.bindingToken += 1
subscription.unsubscribeOwner()
subscription.removeDestroyedListener()
}
function sendEvent(
subscription: RetainedSubscription,
event: GitRepositorySnapshotRevisionEvent,
token: number
): void {
if (
subscriptions.get(subscription.id) !== subscription ||
subscription.bindingToken !== token ||
subscription.sender.isDestroyed()
) {
return
}
const payload: GitRepositorySnapshotSubscriptionEvent = Object.freeze({
...event,
incarnation: subscription.incarnation
})
try {
subscription.sender.send(GIT_REPOSITORY_SNAPSHOT_REVISION_CHANNEL, {
subscriptionId: subscription.id,
event: payload
})
} catch {
closeSubscription(subscription)
}
}
function bindSshProvider(
subscription: RetainedSubscription,
provider: SshGitProvider,
incarnation: number
): void {
subscription.bindingToken += 1
const token = subscription.bindingToken
subscription.incarnation = incarnation
subscription.unsubscribeOwner = provider.subscribeRepositorySnapshot(
subscription.args.worktreePath,
{
includeIgnored: subscription.args.includeIgnored,
bypassEffectiveUpstreamNegativeCache: subscription.args.bypassEffectiveUpstreamNegativeCache,
reuseLineStats: subscription.args.reuseLineStats
},
subscription.args.pushTarget,
(event) => sendEvent(subscription, event, token)
)
}
function handleSshProviderChange(event: {
connectionId: string
generation: number
provider: SshGitProvider | undefined
}): void {
for (const subscription of subscriptions.values()) {
if (subscription.args.connectionId !== event.connectionId) {
continue
}
subscription.bindingToken += 1
subscription.unsubscribeOwner()
subscription.unsubscribeOwner = () => {}
subscription.incarnation = event.generation
sendEvent(
subscription,
{ state: 'invalidated', generation: 0, revision: 0 },
subscription.bindingToken
)
if (event.provider && subscriptions.get(subscription.id) === subscription) {
bindSshProvider(subscription, event.provider, event.generation)
}
}
}
export function disposeGitRepositorySnapshotSubscriptionHandlers(): void {
for (const subscription of Array.from(subscriptions.values())) {
closeSubscription(subscription)
}
disposeRegistryListener?.()
disposeRegistryListener = null
ipcMain.removeHandler(SUBSCRIBE_CHANNEL)
ipcMain.removeHandler(UNSUBSCRIBE_CHANNEL)
}
export function registerGitRepositorySnapshotSubscriptionHandlers(store: Store): void {
disposeGitRepositorySnapshotSubscriptionHandlers()
disposeRegistryListener = subscribeSshGitProviderRegistry(handleSshProviderChange)
ipcMain.handle(
SUBSCRIBE_CHANNEL,
async (
event,
args: GitRepositorySnapshotRequest & { subscriptionId?: string }
): Promise<{ subscriptionId: string }> => {
if (args.pushTarget) {
assertGitPushTargetShape(args.pushTarget)
}
const id =
typeof args.subscriptionId === 'string' && args.subscriptionId.length > 0
? args.subscriptionId
: randomUUID()
if (subscriptions.has(id)) {
throw new Error('Git repository snapshot subscription id already exists')
}
const sender = event.sender
let destroyedListenerAttached = false
const subscription: RetainedSubscription = {
id,
args,
sender,
senderId: sender.id,
incarnation: 0,
bindingToken: 0,
unsubscribeOwner: () => {},
removeDestroyedListener: () => {
if (!destroyedListenerAttached) {
return
}
destroyedListenerAttached = false
sender.removeListener('destroyed', closeOnDestroyed)
}
}
const closeOnDestroyed = (): void => closeSubscription(subscription)
sender.once('destroyed', closeOnDestroyed)
destroyedListenerAttached = true
subscriptions.set(id, subscription)
try {
if (args.connectionId) {
const provider = getSshGitProvider(args.connectionId)
if (!provider) {
throw new Error(SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE)
}
bindSshProvider(subscription, provider, getSshGitProviderGeneration(args.connectionId))
} else {
const request = await resolveLocalGitRepositorySnapshotRequest(store, args)
if (subscriptions.get(subscription.id) !== subscription || sender.isDestroyed()) {
closeSubscription(subscription)
return { subscriptionId: id }
}
const token = subscription.bindingToken
subscription.unsubscribeOwner = subscribeGitRepositorySnapshot(
request.worktreePath,
request.options,
args.pushTarget,
(revisionEvent) => sendEvent(subscription, revisionEvent, token)
)
}
} catch (error) {
closeSubscription(subscription)
throw error
}
if (sender.isDestroyed()) {
closeSubscription(subscription)
}
return { subscriptionId: id }
}
)
ipcMain.handle(
UNSUBSCRIBE_CHANNEL,
(event, args: { subscriptionId: string }): { unsubscribed: boolean } => {
const subscription = subscriptions.get(args.subscriptionId)
if (!subscription || subscription.senderId !== event.sender.id) {
return { unsubscribed: false }
}
closeSubscription(subscription)
return { unsubscribed: true }
}
)
}
export function getGitRepositorySnapshotSubscriptionCountForTests(): number {
return subscriptions.size
}
@@ -28,6 +28,7 @@ const {
registerUIHandlersMock,
setTrustedUIRendererWebContentsIdMock,
registerFilesystemHandlersMock,
registerGitRepositorySnapshotSubscriptionHandlersMock,
registerRuntimeHandlersMock,
registerRuntimeEnvironmentHandlersMock,
registerEphemeralVmHandlersMock,
@@ -93,6 +94,7 @@ const {
registerUIHandlersMock: vi.fn(),
setTrustedUIRendererWebContentsIdMock: vi.fn(),
registerFilesystemHandlersMock: vi.fn(),
registerGitRepositorySnapshotSubscriptionHandlersMock: vi.fn(),
registerRuntimeHandlersMock: vi.fn(),
registerRuntimeEnvironmentHandlersMock: vi.fn(),
registerEphemeralVmHandlersMock: vi.fn(),
@@ -283,6 +285,11 @@ vi.mock('./filesystem', () => ({
registerFilesystemHandlers: registerFilesystemHandlersMock
}))
vi.mock('./git-repository-snapshot-subscriptions', () => ({
registerGitRepositorySnapshotSubscriptionHandlers:
registerGitRepositorySnapshotSubscriptionHandlersMock
}))
vi.mock('./filesystem-watcher', () => ({
registerFilesystemWatcherHandlers: registerFilesystemWatcherHandlersMock
}))
@@ -411,6 +418,7 @@ describe('registerCoreHandlers', () => {
registerUIHandlersMock.mockReset()
setTrustedUIRendererWebContentsIdMock.mockReset()
registerFilesystemHandlersMock.mockReset()
registerGitRepositorySnapshotSubscriptionHandlersMock.mockReset()
registerRuntimeHandlersMock.mockReset()
registerRuntimeEnvironmentHandlersMock.mockReset()
registerEphemeralVmHandlersMock.mockReset()
@@ -533,6 +541,7 @@ describe('registerCoreHandlers', () => {
expect(registerEmulatorFrameStreamHandlersMock).toHaveBeenCalled()
expect(registerEmulatorVideoStreamHandlersMock).toHaveBeenCalled()
expect(registerFilesystemHandlersMock).toHaveBeenCalledWith(store)
expect(registerGitRepositorySnapshotSubscriptionHandlersMock).toHaveBeenCalledWith(store)
expect(registerRuntimeHandlersMock).toHaveBeenCalledWith(runtime)
expect(registerRuntimeEnvironmentHandlersMock).toHaveBeenCalledWith(store)
expect(registerEphemeralVmHandlersMock).toHaveBeenCalledWith(store, undefined)
+2
View File
@@ -6,6 +6,7 @@ import type { Store } from '../persistence'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import type { StatsCollector } from '../stats/collector'
import { registerFilesystemHandlers } from './filesystem'
import { registerGitRepositorySnapshotSubscriptionHandlers } from './git-repository-snapshot-subscriptions'
import type { CommitMessageAgentEnvironmentResolvers } from '../text-generation/commit-message-agent-environment'
import { registerFilesystemWatcherHandlers } from './filesystem-watcher'
import { registerClaudeUsageHandlers } from './claude-usage'
@@ -211,6 +212,7 @@ export function registerCoreHandlers(
} else {
registerFilesystemHandlers(store)
}
registerGitRepositorySnapshotSubscriptionHandlers(store)
registerFilesystemWatcherHandlers()
registerRuntimeHandlers(runtime)
registerRuntimeEnvironmentHandlers(store)
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import {
getSshGitProviderGeneration,
registerSshGitProvider,
subscribeSshGitProviderRegistry,
unregisterSshGitProvider
} from './ssh-git-dispatch'
@@ -25,4 +26,29 @@ describe('SSH Git provider registry', () => {
expect(unregistered).toBe(registered + 1)
expect(reRegistered).toBe(unregistered + 1)
})
it('reports exact replacement generations until the listener unsubscribes', () => {
const events: { generation: number; provider: unknown }[] = []
const unsubscribe = subscribeSshGitProviderRegistry((event) => {
if (event.connectionId === connectionId) {
events.push({ generation: event.generation, provider: event.provider })
}
})
const first = {} as never
const second = {} as never
registerSshGitProvider(connectionId, first)
registerSshGitProvider(connectionId, second)
unregisterSshGitProvider(connectionId)
unsubscribe()
registerSshGitProvider(connectionId, first)
expect(events).toEqual([
{ generation: expect.any(Number), provider: first },
{ generation: expect.any(Number), provider: second },
{ generation: expect.any(Number), provider: undefined }
])
expect(events[1].generation).toBe(events[0].generation + 1)
expect(events[2].generation).toBe(events[1].generation + 1)
})
})
+34 -2
View File
@@ -2,21 +2,43 @@ import type { SshGitProvider } from './ssh-git-provider'
const sshProviders = new Map<string, SshGitProvider>()
const sshProviderGenerations = new Map<string, number>()
const sshProviderRegistryListeners = new Set<
(event: {
connectionId: string
generation: number
provider: SshGitProvider | undefined
}) => void
>()
export const SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE =
'Remote connection dropped. Click Reconnect on the SSH target before retrying.'
export function registerSshGitProvider(connectionId: string, provider: SshGitProvider): void {
sshProviders.set(connectionId, provider)
sshProviderGenerations.set(connectionId, (sshProviderGenerations.get(connectionId) ?? 0) + 1)
const generation = (sshProviderGenerations.get(connectionId) ?? 0) + 1
sshProviderGenerations.set(connectionId, generation)
notifySshGitProviderRegistry({ connectionId, generation, provider })
}
export function unregisterSshGitProvider(connectionId: string): void {
if (sshProviders.delete(connectionId)) {
sshProviderGenerations.set(connectionId, (sshProviderGenerations.get(connectionId) ?? 0) + 1)
const generation = (sshProviderGenerations.get(connectionId) ?? 0) + 1
sshProviderGenerations.set(connectionId, generation)
notifySshGitProviderRegistry({ connectionId, generation, provider: undefined })
}
}
export function subscribeSshGitProviderRegistry(
listener: (event: {
connectionId: string
generation: number
provider: SshGitProvider | undefined
}) => void
): () => void {
sshProviderRegistryListeners.add(listener)
return () => sshProviderRegistryListeners.delete(listener)
}
export function getSshGitProviderGeneration(connectionId: string): number {
return sshProviderGenerations.get(connectionId) ?? 0
}
@@ -32,3 +54,13 @@ export function requireSshGitProvider(connectionId: string): SshGitProvider {
}
return provider
}
function notifySshGitProviderRegistry(event: {
connectionId: string
generation: number
provider: SshGitProvider | undefined
}): void {
for (const listener of sshProviderRegistryListeners) {
listener(event)
}
}
+25
View File
@@ -19,6 +19,7 @@ import type {
RemoveWorktreeResult
} from '../../shared/types'
import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history'
import type { GitRepositorySnapshotRevisionEvent } from '../../shared/git-repository-snapshot'
import { buildHostedRemoteCommitUrl, buildHostedRemoteFileUrl } from '../git/hosted-remote-url'
import { JsonRpcErrorCode } from '../ssh/relay-protocol'
import { requestGitStreamable } from '../ssh/ssh-git-response-stream-reader'
@@ -548,6 +549,30 @@ export class SshGitProvider implements IGitProvider {
})
}
subscribeRepositorySnapshot(
worktreePath: string,
options: GitProviderStatusOptions | undefined,
pushTarget: GitPushTarget | undefined,
listener: (event: GitRepositorySnapshotRevisionEvent) => void
): () => void {
return this.repositorySnapshotOwner.subscribe(
{
executionIdentity: { kind: 'ssh-provider', connectionId: this.connectionId },
worktreePath,
statusIdentity: {
includeIgnored: options?.includeIgnored === true,
reuseLineStats: options?.reuseLineStats === true,
bypassEffectiveUpstreamNegativeCache:
options?.bypassEffectiveUpstreamNegativeCache === true,
limit: null,
sharedLinkPaths: []
},
pushTarget
},
listener
)
}
async pushBranch(
worktreePath: string,
publish = false,
+9 -1
View File
@@ -282,7 +282,11 @@ import type {
import type { SetupScriptImportCandidate } from '../shared/setup-script-imports'
import type { GitHistoryOptions, GitHistoryResult } from '../shared/git-history'
import type { GitRepositorySnapshot } from '../shared/git-repository-snapshot'
import type {
GitRepositorySnapshot,
GitRepositorySnapshotRequest,
GitRepositorySnapshotSubscriptionEvent
} from '../shared/git-repository-snapshot'
import type { PublicKnownRuntimeEnvironment } from '../shared/runtime-environments'
import type { EphemeralVmRecipeDoctorResult } from '../shared/ephemeral-vm-recipes'
import type { EphemeralVmRecipeResultWarning } from '../shared/ephemeral-vm-recipe-diagnostics'
@@ -2846,6 +2850,10 @@ export type PreloadApi = {
reuseLineStats?: boolean
pushTarget?: GitPushTarget
}) => Promise<GitRepositorySnapshot | null>
subscribeRepositorySnapshot: (
args: GitRepositorySnapshotRequest,
callback: (event: GitRepositorySnapshotSubscriptionEvent) => void
) => Promise<{ unsubscribe: () => void }>
submoduleStatus: (args: {
worktreePath: string
submodulePath: string
@@ -0,0 +1,103 @@
import { describe, expect, it, vi } from 'vitest'
import { subscribeGitRepositorySnapshotFromPreload } from './git-repository-snapshot-subscriptions'
type Payload = {
subscriptionId: string
event: {
state: 'ready' | 'invalidated'
generation: number
revision: number
incarnation: number
}
}
function createIpc() {
const listeners = new Set<(event: unknown, payload: Payload) => void>()
return {
invoke: vi.fn(async (_channel: string, args: unknown) => args),
on: vi.fn((_channel: string, listener: (event: unknown, payload: Payload) => void) => {
listeners.add(listener)
}),
removeListener: vi.fn(
(_channel: string, listener: (event: unknown, payload: Payload) => void) => {
listeners.delete(listener)
}
),
emit: (payload: Payload): void => {
for (const listener of Array.from(listeners)) {
listener(null, payload)
}
},
listenerCount: (): number => listeners.size
}
}
describe('subscribeGitRepositorySnapshotFromPreload', () => {
it('installs exact listeners before subscribe and isolates callback ids', async () => {
const ipc = createIpc()
const first = vi.fn()
const second = vi.fn()
const firstHandle = await subscribeGitRepositorySnapshotFromPreload(
ipc,
{ worktreePath: '/one' },
first,
() => 'sub-1'
)
const secondHandle = await subscribeGitRepositorySnapshotFromPreload(
ipc,
{ worktreePath: '/two' },
second,
() => 'sub-2'
)
expect(ipc.on).toHaveBeenCalledTimes(2)
expect(ipc.on.mock.invocationCallOrder[0]).toBeLessThan(ipc.invoke.mock.invocationCallOrder[0])
ipc.emit({
subscriptionId: 'sub-2',
event: { state: 'ready', generation: 1, revision: 2, incarnation: 3 }
})
expect(first).not.toHaveBeenCalled()
expect(second).toHaveBeenCalledTimes(1)
firstHandle.unsubscribe()
expect(ipc.listenerCount()).toBe(1)
secondHandle.unsubscribe()
expect(ipc.listenerCount()).toBe(0)
expect(ipc.invoke).toHaveBeenCalledWith('git:unsubscribeRepositorySnapshot', {
subscriptionId: 'sub-2'
})
})
it('releases the dispatcher when subscribe rejects', async () => {
const ipc = createIpc()
ipc.invoke.mockRejectedValueOnce(new Error('unavailable'))
await expect(
subscribeGitRepositorySnapshotFromPreload(
ipc,
{ worktreePath: '/repo' },
vi.fn(),
() => 'sub-error'
)
).rejects.toThrow('unavailable')
expect(ipc.listenerCount()).toBe(0)
expect(ipc.removeListener).toHaveBeenCalledTimes(1)
})
it('makes explicit unsubscribe idempotent', async () => {
const ipc = createIpc()
const handle = await subscribeGitRepositorySnapshotFromPreload(
ipc,
{ worktreePath: '/repo' },
vi.fn(),
() => 'sub-1'
)
handle.unsubscribe()
handle.unsubscribe()
expect(ipc.invoke).toHaveBeenCalledTimes(2)
})
})
@@ -0,0 +1,69 @@
import type {
GitRepositorySnapshotRequest,
GitRepositorySnapshotSubscriptionEvent
} from '../shared/git-repository-snapshot'
export type GitRepositorySnapshotSubscriptionHandle = {
unsubscribe: () => void
}
type SubscriptionPayload = {
subscriptionId: string
event: GitRepositorySnapshotSubscriptionEvent
}
type GitRepositorySnapshotSubscriptionIpc = {
invoke: (channel: string, args: unknown) => Promise<unknown>
on: (channel: string, listener: (event: unknown, payload: SubscriptionPayload) => void) => void
removeListener: (
channel: string,
listener: (event: unknown, payload: SubscriptionPayload) => void
) => void
}
const REVISION_CHANNEL = 'git:repositorySnapshotRevision'
function createSubscriptionId(): string {
return globalThis.crypto.randomUUID()
}
export async function subscribeGitRepositorySnapshotFromPreload(
ipc: GitRepositorySnapshotSubscriptionIpc,
args: GitRepositorySnapshotRequest,
callback: (event: GitRepositorySnapshotSubscriptionEvent) => void,
createId = createSubscriptionId
): Promise<GitRepositorySnapshotSubscriptionHandle> {
const subscriptionId = createId()
let closed = false
const listener = (_event: unknown, payload: SubscriptionPayload): void => {
if (payload.subscriptionId === subscriptionId) {
callback(payload.event)
}
}
const release = (): boolean => {
if (closed) {
return false
}
closed = true
ipc.removeListener(REVISION_CHANNEL, listener)
return true
}
ipc.on(REVISION_CHANNEL, listener)
try {
await ipc.invoke('git:subscribeRepositorySnapshot', {
...args,
subscriptionId
})
} catch (error) {
release()
throw error
}
return {
unsubscribe: () => {
if (!release()) {
return
}
void ipc.invoke('git:unsubscribeRepositorySnapshot', { subscriptionId })
}
}
}
+14 -1
View File
@@ -14,7 +14,11 @@ import type { AgentHookInstallStatus } from '../shared/agent-hook-types'
import type { CodexConfigSyncStatus } from '../shared/codex-config-sync-types'
import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { GitRepositorySnapshot } from '../shared/git-repository-snapshot'
import type {
GitRepositorySnapshot,
GitRepositorySnapshotRequest,
GitRepositorySnapshotSubscriptionEvent
} from '../shared/git-repository-snapshot'
import type { StartupCommandDelivery } from '../shared/codex-startup-delivery'
import type {
AgentProviderSessionMetadata,
@@ -255,6 +259,10 @@ import type {
LocalLogTailWatchArgs
} from '../shared/local-log-tail-types'
import { subscribeRuntimeEnvironmentFromPreload } from './runtime-environment-subscriptions'
import {
subscribeGitRepositorySnapshotFromPreload,
type GitRepositorySnapshotSubscriptionHandle
} from './git-repository-snapshot-subscriptions'
import type { RuntimeEnvironmentSubscriptionHandle } from './runtime-environment-subscriptions'
import type { HostedReviewForBranchArgs } from '../shared/hosted-review'
import type { ReadClipboardTextOptions } from '../shared/clipboard-text'
@@ -3184,6 +3192,11 @@ const api = {
reuseLineStats?: boolean
pushTarget?: GitPushTarget
}): Promise<GitRepositorySnapshot | null> => ipcRenderer.invoke('git:repositorySnapshot', args),
subscribeRepositorySnapshot: (
args: GitRepositorySnapshotRequest,
callback: (event: GitRepositorySnapshotSubscriptionEvent) => void
): Promise<GitRepositorySnapshotSubscriptionHandle> =>
subscribeGitRepositorySnapshotFromPreload(ipcRenderer, args, callback),
submoduleStatus: (args: {
worktreePath: string
submodulePath: string
@@ -84,7 +84,6 @@ import type {
HostedReviewProvider
} from '../../../../shared/hosted-review'
import { resolveHostedReviewCreationProvider } from '../../../../shared/hosted-review-creation-providers'
import { normalizeGlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime'
import { normalizeHostedReviewHeadRef } from '../../../../shared/hosted-review-refs'
import { getHostedReviewCacheKey, refreshHostedReviewCard } from '@/store/slices/hosted-review'
import { toast } from 'sonner'
@@ -142,6 +141,7 @@ import {
resolveChecksPanelReviewEvidenceProvider
} from './checks-panel-pr-refresh-request'
import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
import { useChecksPanelRepositorySnapshotRevision } from './use-checks-panel-repository-snapshot-revision'
import { useMountedRef } from '@/hooks/useMountedRef'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { gitLabPipelineJobsToPRChecks } from '../../../../shared/gitlab-pipeline-checks'
@@ -518,6 +518,10 @@ export default function ChecksPanel(): React.JSX.Element {
null
)
const [gitStatusRefreshNonce, setGitStatusRefreshNonce] = useState(0)
const requestGitStatusRefresh = useCallback(
() => setGitStatusRefreshNonce((value) => value + 1),
[]
)
// Bumped by manual Retry/Refresh so eligibility re-runs even when Git state is unchanged (e.g. an auth fix must still clear the hard error).
const [eligibilityRefreshNonce, setEligibilityRefreshNonce] = useState(0)
const [editingTitle, setEditingTitle] = useState(false)
@@ -601,14 +605,16 @@ export default function ChecksPanel(): React.JSX.Element {
[runtimeEnvironmentId, settings]
)
const repoConnectionId = repo?.connectionId?.trim() || null
// Local execution host variant (wsl:{distro} vs host); applies only when local — remote contexts are scoped by runtimeEnvironmentId/connectionId.
const localExecutionScope = useMemo<string | null>(() => {
const localExecutionScope = useAppStore((state): string | null => {
if (runtimeEnvironmentId != null || repoConnectionId != null) {
return null
}
const localRuntime = normalizeGlobalWindowsRuntimeDefault(settings?.localWindowsRuntimeDefault)
return localRuntime.kind === 'wsl' ? `wsl:${localRuntime.distro ?? ''}` : 'host'
}, [runtimeEnvironmentId, repoConnectionId, settings?.localWindowsRuntimeDefault])
const localRuntime = getLocalProjectExecutionRuntimeContext(state, activeWorktreeId)
if (localRuntime?.status === 'repair-required') {
return `repair:${localRuntime.repair.cacheKey}`
}
return localRuntime?.runtime.kind === 'wsl' ? `wsl:${localRuntime.runtime.distro}` : 'host'
})
const sshConnectionStatus = useAppStore((s) =>
repoConnectionId ? s.sshConnectionStates.get(repoConnectionId)?.status : undefined
)
@@ -685,6 +691,30 @@ export default function ChecksPanel(): React.JSX.Element {
}
const isFolder = repo ? isFolderRepo(repo) : false
const gitStatusSnapshotContext = useMemo(
() => ({
settings: ownerSettings,
worktreeId: activeWorktreeId,
worktreePath: activeWorktreePath ?? '',
connectionId: activeConnectionId ?? undefined
}),
[activeConnectionId, activeWorktreeId, activeWorktreePath, ownerSettings]
)
const gitStatusSnapshotRevision = useChecksPanelRepositorySnapshotRevision({
context: gitStatusSnapshotContext,
contextKey: panelContextKey,
enabled:
Boolean(repo) &&
!isFolder &&
Boolean(branch) &&
isPanelVisible &&
Boolean(activeWorktreeId) &&
Boolean(activeWorktreePath) &&
!runtimeEnvironmentId &&
(!repoConnectionId || sshConnectionStatus === 'connected'),
pushTarget: activeWorktreePushTarget,
requestRefresh: requestGitStatusRefresh
})
const prCacheKey =
repo && branch
? getGitHubPRCacheKey(
@@ -1498,7 +1528,8 @@ export default function ChecksPanel(): React.JSX.Element {
}
}
gitStatusSnapshotInFlightContextRef.current = requestContextKey
// Why: global status maps are keyed only by worktree; use their changes as invalidation signals, then fetch a local snapshot.
const revisionRead = gitStatusSnapshotRevision.beginRead()
let observedRepositoryRevision: number | null = null
if (gitStatusSnapshotRetryTimerRef.current) {
clearTimeout(gitStatusSnapshotRetryTimerRef.current)
gitStatusSnapshotRetryTimerRef.current = null
@@ -1513,9 +1544,17 @@ export default function ChecksPanel(): React.JSX.Element {
connectionId
}
void (async () => {
const readOwnerSnapshot = async (
options?: Parameters<typeof getDesktopGitRepositorySnapshot>[1]
) => {
const snapshot = await getDesktopGitRepositorySnapshot(context, options)
if (snapshot) {
observedRepositoryRevision = Math.max(observedRepositoryRevision ?? 0, snapshot.revision)
}
return snapshot
}
let repositorySnapshot = readChecksPanelRepositorySnapshot(
await getDesktopGitRepositorySnapshot(
context,
await readOwnerSnapshot(
activeWorktreePushTarget ? { pushTarget: activeWorktreePushTarget } : undefined
),
requestContextKey,
@@ -1523,7 +1562,7 @@ export default function ChecksPanel(): React.JSX.Element {
)
if (!repositorySnapshot) {
repositorySnapshot = readChecksPanelRepositorySnapshot(
await getDesktopGitRepositorySnapshot(context, {
await readOwnerSnapshot({
reuseLineStats: true,
...(activeWorktreePushTarget ? { pushTarget: activeWorktreePushTarget } : {})
}),
@@ -1562,6 +1601,9 @@ export default function ChecksPanel(): React.JSX.Element {
) {
freshRemoteStatus = await getRuntimeGitUpstreamStatus(context)
}
await readOwnerSnapshot(
activeWorktreePushTarget ? { pushTarget: activeWorktreePushTarget } : undefined
)
return {
contextKey: requestContextKey,
hasUncommittedChanges: status.entries.length > 0,
@@ -1612,12 +1654,19 @@ export default function ChecksPanel(): React.JSX.Element {
if (gitStatusSnapshotInFlightContextRef.current === requestContextKey) {
gitStatusSnapshotInFlightContextRef.current = null
}
let shouldRerun = gitStatusSnapshotRevision.finishRead(
revisionRead,
observedRepositoryRevision
)
if (gitStatusSnapshotRerunContextRef.current === requestContextKey) {
gitStatusSnapshotRerunContextRef.current = null
shouldRerun = true
}
if (shouldRerun) {
if (
shouldCommitChecksPanelGitStatusSnapshot(panelContextKeyRef.current, requestContextKey)
) {
setGitStatusRefreshNonce((value) => value + 1)
requestGitStatusRefresh()
}
}
})
@@ -1634,7 +1683,7 @@ export default function ChecksPanel(): React.JSX.Element {
activeWorktreePath,
activeConnectionId,
branch,
gitStatusInvalidation,
gitStatusSnapshotRevision,
gitStatusRefreshNonce,
isFolder,
isPanelVisible,
@@ -1642,7 +1691,7 @@ export default function ChecksPanel(): React.JSX.Element {
panelContextKey,
repo,
repoConnectionId,
remoteStatusInvalidation,
requestGitStatusRefresh,
runtimeEnvironmentId,
sshConnectionStatus,
updateWorktreeGitIdentity
@@ -106,6 +106,23 @@ describe('buildChecksPanelGitStatusContextKey', () => {
expect(context(target)).not.toBe(context({ ...target, remoteUrl: 'ssh://git.example/repo' }))
})
it('distinguishes the exact local execution host and WSL distro', () => {
const base = {
repoId: 'repo-1',
worktreeId: 'worktree-1',
worktreePath: 'repo-worktree',
branch: 'feature/checks',
runtimeEnvironmentId: null,
repoConnectionId: null,
pushTarget: null
}
const context = (localExecutionScope: string) =>
buildChecksPanelGitStatusContextKey({ ...base, localExecutionScope })
expect(context('host')).not.toBe(context('wsl:Ubuntu'))
expect(context('wsl:Ubuntu')).not.toBe(context('wsl:Debian'))
})
it('changes when linked hosted review metadata changes', () => {
const base = {
repoId: 'repo-1',
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest'
import type { GitRepositorySnapshotSubscriptionEvent } from '../../../../shared/git-repository-snapshot'
import { ChecksPanelRepositorySnapshotRevisionGate } from './checks-panel-repository-snapshot-revision-gate'
function event(
revision: number,
state: 'invalidated' | 'ready' = 'ready',
generation = 0,
incarnation = 0
): GitRepositorySnapshotSubscriptionEvent {
return { state, generation, revision, incarnation }
}
describe('ChecksPanelRepositorySnapshotRevisionGate', () => {
it('requests one refresh for a newer ready revision while idle', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
expect(gate.observe(event(1))).toBe(true)
expect(gate.observe(event(1))).toBe(false)
expect(gate.observe(event(2))).toBe(false)
})
it('consumes a self-published ready revision observed by the active read', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
const read = gate.begin()
expect(gate.observe(event(4))).toBe(false)
expect(gate.finish(read, 4)).toBe(false)
})
it('requests a trailing refresh for a newer publication than the active read observed', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
const read = gate.begin()
expect(gate.observe(event(5))).toBe(false)
expect(gate.observe(event(6))).toBe(false)
expect(gate.finish(read, 5)).toBe(true)
})
it('does not consume a replacement-provider publication with an old read revision', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
const read = gate.begin()
expect(gate.observe(event(0, 'invalidated', 0, 2))).toBe(false)
expect(gate.observe(event(2, 'ready', 0, 2))).toBe(false)
expect(gate.finish(read, 20)).toBe(true)
})
it('advances invalidation fences without launching a premature refresh', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
expect(gate.observe(event(3, 'invalidated', 1))).toBe(false)
expect(gate.observe(event(2, 'ready', 0))).toBe(false)
expect(gate.observe(event(4, 'ready', 1))).toBe(true)
})
it('resets context-local scheduling state', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
expect(gate.observe(event(1))).toBe(true)
gate.reset()
expect(gate.observe(event(1))).toBe(true)
})
it('ignores a read that settles after a context reset', () => {
const gate = new ChecksPanelRepositorySnapshotRevisionGate()
const staleRead = gate.begin()
gate.reset()
const currentRead = gate.begin()
expect(gate.observe(event(2))).toBe(false)
expect(gate.finish(staleRead, 20)).toBe(false)
expect(gate.finish(currentRead, 1)).toBe(true)
})
})
@@ -0,0 +1,90 @@
import type { GitRepositorySnapshotSubscriptionEvent } from '../../../../shared/git-repository-snapshot'
function compareEvent(
left: GitRepositorySnapshotSubscriptionEvent,
right: GitRepositorySnapshotSubscriptionEvent
): number {
return (
left.incarnation - right.incarnation ||
left.generation - right.generation ||
left.revision - right.revision
)
}
export class ChecksPanelRepositorySnapshotRevisionGate {
private floor: GitRepositorySnapshotSubscriptionEvent | null = null
private pending: GitRepositorySnapshotSubscriptionEvent | null = null
private inFlight = false
private invalidatedDuringFlight = false
private refreshRequested = false
private readSequence = 0
private activeRead: number | null = null
reset(): void {
this.readSequence += 1
this.activeRead = null
this.floor = null
this.pending = null
this.inFlight = false
this.invalidatedDuringFlight = false
this.refreshRequested = false
}
begin(): number {
this.readSequence += 1
this.activeRead = this.readSequence
this.inFlight = true
this.invalidatedDuringFlight = false
this.refreshRequested = false
return this.readSequence
}
observe(event: GitRepositorySnapshotSubscriptionEvent): boolean {
if (this.floor && compareEvent(event, this.floor) <= 0) {
return false
}
this.floor = event
if (event.state === 'invalidated') {
if (this.inFlight) {
this.invalidatedDuringFlight = true
}
if (this.pending && compareEvent(this.pending, event) <= 0) {
this.pending = null
}
return false
}
if (!this.pending || compareEvent(event, this.pending) > 0) {
this.pending = event
}
if (this.inFlight || this.refreshRequested) {
return false
}
this.refreshRequested = true
return true
}
finish(read: number, observedRevision: number | null): boolean {
if (this.activeRead !== read) {
return false
}
this.activeRead = null
this.inFlight = false
const pending = this.pending
if (
!pending ||
(!this.invalidatedDuringFlight &&
observedRevision !== null &&
observedRevision >= pending.revision)
) {
this.pending = null
this.invalidatedDuringFlight = false
return false
}
this.invalidatedDuringFlight = false
if (this.refreshRequested) {
return false
}
this.refreshRequested = true
return true
}
}
@@ -0,0 +1,147 @@
// @vitest-environment happy-dom
import { act, renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GitRepositorySnapshotSubscriptionEvent } from '../../../../shared/git-repository-snapshot'
import { useChecksPanelRepositorySnapshotRevision } from './use-checks-panel-repository-snapshot-revision'
const subscribe = vi.fn()
vi.mock('@/runtime/desktop-git-repository-snapshot-client', () => ({
subscribeDesktopGitRepositorySnapshot: (
...args: Parameters<typeof subscribe>
): ReturnType<typeof subscribe> => subscribe(...args)
}))
const context = {
settings: { activeRuntimeEnvironmentId: null },
worktreeId: 'wt-1',
worktreePath: '/repo'
}
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
let resolve!: (value: T) => void
const promise = new Promise<T>((complete) => {
resolve = complete
})
return { promise, resolve }
}
describe('useChecksPanelRepositorySnapshotRevision', () => {
beforeEach(() => {
subscribe.mockReset()
})
it('subscribes to both polling identities and tears down on context change', async () => {
const unsubscribes = [vi.fn(), vi.fn(), vi.fn(), vi.fn()]
subscribe.mockImplementation(async () => ({
unsubscribe: unsubscribes[subscribe.mock.calls.length - 1]
}))
const requestRefresh = vi.fn()
const { rerender, unmount } = renderHook(
({ contextKey }) =>
useChecksPanelRepositorySnapshotRevision({
context,
contextKey,
enabled: true,
pushTarget: null,
requestRefresh
}),
{ initialProps: { contextKey: 'one' } }
)
await waitFor(() => expect(subscribe).toHaveBeenCalledTimes(2))
expect(subscribe.mock.calls.map((call) => call[1])).toEqual([{}, { reuseLineStats: true }])
rerender({ contextKey: 'two' })
await waitFor(() => expect(subscribe).toHaveBeenCalledTimes(4))
expect(unsubscribes[0]).toHaveBeenCalledOnce()
expect(unsubscribes[1]).toHaveBeenCalledOnce()
unmount()
expect(unsubscribes[2]).toHaveBeenCalledOnce()
expect(unsubscribes[3]).toHaveBeenCalledOnce()
})
it('coalesces ready delivery and consumes self-publication without another refresh', async () => {
const callbacks: ((event: GitRepositorySnapshotSubscriptionEvent) => void)[] = []
subscribe.mockImplementation(async (_context, _options, callback) => {
callbacks.push(callback)
return { unsubscribe: vi.fn() }
})
const requestRefresh = vi.fn()
const { result } = renderHook(() =>
useChecksPanelRepositorySnapshotRevision({
context,
contextKey: 'one',
enabled: true,
pushTarget: null,
requestRefresh
})
)
await waitFor(() => expect(callbacks).toHaveLength(2))
const ready = { state: 'ready', generation: 0, revision: 1, incarnation: 0 } as const
act(() => {
callbacks[0](ready)
callbacks[1](ready)
})
expect(requestRefresh).toHaveBeenCalledOnce()
let read = 0
act(() => {
read = result.current.beginRead()
})
act(() => callbacks[0]({ ...ready, revision: 2 }))
expect(result.current.finishRead(read, 2)).toBe(false)
expect(requestRefresh).toHaveBeenCalledOnce()
})
it('does not subscribe while disabled and cleans a partial subscription failure', async () => {
const unsubscribe = vi.fn()
subscribe.mockResolvedValueOnce({ unsubscribe }).mockRejectedValueOnce(new Error('unavailable'))
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const { rerender } = renderHook(
({ enabled }) =>
useChecksPanelRepositorySnapshotRevision({
context,
contextKey: 'one',
enabled,
pushTarget: null,
requestRefresh: vi.fn()
}),
{ initialProps: { enabled: false } }
)
expect(subscribe).not.toHaveBeenCalled()
rerender({ enabled: true })
await waitFor(() => expect(warn).toHaveBeenCalledOnce())
expect(unsubscribe).toHaveBeenCalledOnce()
rerender({ enabled: false })
expect(unsubscribe).toHaveBeenCalledOnce()
warn.mockRestore()
})
it('cleans a settled handle while the sibling subscription is still pending', async () => {
const pending = deferred<{ unsubscribe: () => void }>()
const firstUnsubscribe = vi.fn()
const secondUnsubscribe = vi.fn()
subscribe
.mockReturnValueOnce(pending.promise)
.mockResolvedValueOnce({ unsubscribe: secondUnsubscribe })
const { unmount } = renderHook(() =>
useChecksPanelRepositorySnapshotRevision({
context,
contextKey: 'one',
enabled: true,
pushTarget: null,
requestRefresh: vi.fn()
})
)
await waitFor(() => expect(subscribe).toHaveBeenCalledTimes(2))
unmount()
expect(secondUnsubscribe).toHaveBeenCalledOnce()
pending.resolve({ unsubscribe: firstUnsubscribe })
await waitFor(() => expect(firstUnsubscribe).toHaveBeenCalledOnce())
})
})
@@ -0,0 +1,94 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import type { GitPushTarget } from '../../../../shared/types'
import {
subscribeDesktopGitRepositorySnapshot,
type DesktopGitRepositorySnapshotContext
} from '@/runtime/desktop-git-repository-snapshot-client'
import { ChecksPanelRepositorySnapshotRevisionGate } from './checks-panel-repository-snapshot-revision-gate'
type Input = {
context: DesktopGitRepositorySnapshotContext
contextKey: string
enabled: boolean
pushTarget: GitPushTarget | null
requestRefresh: () => void
}
export function useChecksPanelRepositorySnapshotRevision(input: Input): {
beginRead: () => number
finishRead: (read: number, observedRevision: number | null) => boolean
} {
const { context, contextKey, enabled, pushTarget, requestRefresh } = input
const gateRef = useRef(new ChecksPanelRepositorySnapshotRevisionGate())
useEffect(() => {
const gate = gateRef.current
gate.reset()
if (!enabled) {
return undefined
}
let stale = false
const handles: { unsubscribe: () => void }[] = []
const subscribe = async (): Promise<void> => {
const options = pushTarget ? { pushTarget } : {}
const callback = (event: Parameters<typeof gate.observe>[0]): void => {
if (!stale && gate.observe(event)) {
requestRefresh()
}
}
const retain = async (
request: ReturnType<typeof subscribeDesktopGitRepositorySnapshot>
): Promise<void> => {
const handle = await request
if (!handle) {
return
}
if (stale) {
handle.unsubscribe()
return
}
handles.push(handle)
}
const settled = await Promise.allSettled([
retain(subscribeDesktopGitRepositorySnapshot(context, options, callback)),
retain(
subscribeDesktopGitRepositorySnapshot(
context,
{ ...options, reuseLineStats: true },
callback
)
)
])
const rejected = settled.find(
(result): result is PromiseRejectedResult => result.status === 'rejected'
)
if (rejected) {
for (const handle of handles.splice(0)) {
handle.unsubscribe()
}
throw rejected.reason
}
}
void subscribe().catch((error) => {
if (!stale) {
console.warn('[ChecksPanel] repository snapshot subscription failed', error)
}
})
return () => {
stale = true
for (const handle of handles) {
handle.unsubscribe()
}
gate.reset()
}
}, [context, contextKey, enabled, pushTarget, requestRefresh])
const beginRead = useCallback((): number => gateRef.current.begin(), [])
const finishRead = useCallback(
(read: number, observedRevision: number | null): boolean =>
gateRef.current.finish(read, observedRevision),
[]
)
return useMemo(() => ({ beginRead, finishRead }), [beginRead, finishRead])
}
@@ -1,21 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getDesktopGitRepositorySnapshot } from './desktop-git-repository-snapshot-client'
import {
getDesktopGitRepositorySnapshot,
subscribeDesktopGitRepositorySnapshot
} from './desktop-git-repository-snapshot-client'
const repositorySnapshot = vi.fn()
const subscribeRepositorySnapshot = vi.fn()
const runtimeEnvironmentCall = vi.fn()
beforeEach(() => {
repositorySnapshot.mockReset()
subscribeRepositorySnapshot.mockReset()
runtimeEnvironmentCall.mockReset()
vi.stubGlobal('window', {
api: {
git: { repositorySnapshot },
git: { repositorySnapshot, subscribeRepositorySnapshot },
runtimeEnvironments: { call: runtimeEnvironmentCall }
}
})
})
describe('getDesktopGitRepositorySnapshot', () => {
it('subscribes with exact desktop host, option, and push-target identity', async () => {
const handle = { unsubscribe: vi.fn() }
const callback = vi.fn()
const pushTarget = {
remoteName: 'fork',
branchName: 'feature/checks',
remoteCreated: false
}
subscribeRepositorySnapshot.mockResolvedValue(handle)
await expect(
subscribeDesktopGitRepositorySnapshot(
{
settings: { activeRuntimeEnvironmentId: null },
worktreeId: 'wt-1',
worktreePath: '/repo',
connectionId: 'ssh-1'
},
{ reuseLineStats: true, pushTarget },
callback
)
).resolves.toBe(handle)
expect(subscribeRepositorySnapshot).toHaveBeenCalledWith(
{
worktreePath: '/repo',
connectionId: 'ssh-1',
reuseLineStats: true,
pushTarget
},
callback
)
})
it('reads the exact desktop host snapshot identity', async () => {
repositorySnapshot.mockResolvedValue(null)
const pushTarget = {
@@ -88,4 +127,20 @@ describe('getDesktopGitRepositorySnapshot', () => {
expect(repositorySnapshot).not.toHaveBeenCalled()
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
})
it('does not subscribe through desktop transport for active runtime environments', async () => {
await expect(
subscribeDesktopGitRepositorySnapshot(
{
settings: { activeRuntimeEnvironmentId: 'env-1' },
worktreeId: 'wt-1',
worktreePath: '/repo'
},
{},
vi.fn()
)
).resolves.toBeNull()
expect(subscribeRepositorySnapshot).not.toHaveBeenCalled()
})
})
@@ -1,15 +1,42 @@
import type { GitPushTarget } from '../../../shared/types'
import type { GitRepositorySnapshot } from '../../../shared/git-repository-snapshot'
import type {
GitRepositorySnapshot,
GitRepositorySnapshotRequest,
GitRepositorySnapshotSubscriptionEvent
} from '../../../shared/git-repository-snapshot'
import { splitWorktreeIdForFilesystem } from '../../../shared/worktree-id'
import { getActiveRuntimeTarget } from './runtime-rpc-client'
import type { RuntimeGitContext } from './runtime-git-client'
export type DesktopGitRepositorySnapshotContext = RuntimeGitContext
function resolveDesktopSnapshotWorktreePath(context: RuntimeGitContext): string {
return context.worktreeId
? (splitWorktreeIdForFilesystem(context.worktreeId)?.worktreePath ?? context.worktreePath)
: context.worktreePath
}
function createDesktopSnapshotRequest(
context: RuntimeGitContext,
options: {
includeIgnored?: boolean
bypassEffectiveUpstreamNegativeCache?: boolean
reuseLineStats?: boolean
pushTarget?: GitPushTarget
}
): GitRepositorySnapshotRequest {
return {
worktreePath: resolveDesktopSnapshotWorktreePath(context),
connectionId: context.connectionId,
...(options.includeIgnored ? { includeIgnored: true } : {}),
...(options.bypassEffectiveUpstreamNegativeCache
? { bypassEffectiveUpstreamNegativeCache: true }
: {}),
...(options.reuseLineStats ? { reuseLineStats: true } : {}),
...(options.pushTarget ? { pushTarget: options.pushTarget } : {})
}
}
export async function getDesktopGitRepositorySnapshot(
context: RuntimeGitContext,
options: {
@@ -23,14 +50,25 @@ export async function getDesktopGitRepositorySnapshot(
if (target.kind !== 'local' && context.worktreeId) {
return null
}
return window.api.git.repositorySnapshot({
worktreePath: resolveDesktopSnapshotWorktreePath(context),
connectionId: context.connectionId,
...(options.includeIgnored ? { includeIgnored: true } : {}),
...(options.bypassEffectiveUpstreamNegativeCache
? { bypassEffectiveUpstreamNegativeCache: true }
: {}),
...(options.reuseLineStats ? { reuseLineStats: true } : {}),
...(options.pushTarget ? { pushTarget: options.pushTarget } : {})
})
return window.api.git.repositorySnapshot(createDesktopSnapshotRequest(context, options))
}
export async function subscribeDesktopGitRepositorySnapshot(
context: RuntimeGitContext,
options: {
includeIgnored?: boolean
bypassEffectiveUpstreamNegativeCache?: boolean
reuseLineStats?: boolean
pushTarget?: GitPushTarget
},
callback: (event: GitRepositorySnapshotSubscriptionEvent) => void
): Promise<{ unsubscribe: () => void } | null> {
const target = getActiveRuntimeTarget(context.settings)
if (target.kind !== 'local' && context.worktreeId) {
return null
}
return window.api.git.subscribeRepositorySnapshot(
createDesktopSnapshotRequest(context, options),
callback
)
}
+24 -1
View File
@@ -1,4 +1,18 @@
import type { GitConflictOperation, GitStatusEntry, GitUpstreamStatus } from './types'
import type {
GitConflictOperation,
GitPushTarget,
GitStatusEntry,
GitUpstreamStatus
} from './types'
export type GitRepositorySnapshotRequest = {
worktreePath: string
connectionId?: string
includeIgnored?: boolean
bypassEffectiveUpstreamNegativeCache?: boolean
reuseLineStats?: boolean
pushTarget?: GitPushTarget
}
export type GitRepositoryProjectionFreshness = Readonly<{
state: 'missing' | 'fresh' | 'stale' | 'failed' | 'placeholder'
@@ -8,6 +22,15 @@ export type GitRepositoryProjectionFreshness = Readonly<{
identity: string | null
}>
export type GitRepositorySnapshotRevisionEvent = Readonly<{
state: 'invalidated' | 'ready'
generation: number
revision: number
}>
export type GitRepositorySnapshotSubscriptionEvent = GitRepositorySnapshotRevisionEvent &
Readonly<{ incarnation: number }>
export type GitRepositorySnapshot = Readonly<{
revision: number
generatedAt: number
@@ -0,0 +1,173 @@
# Phase 2 Git repository snapshot revision subscription — 2026-07-30
## Decision
Retain the host-to-renderer generation/revision subscription and migrate automatic desktop
Checks invalidation from renderer Git-status/upstream store references to the host-owned
`GitRepositorySnapshot` seam. The active poller remains the producer, and a ready event only
causes Checks to query the immutable projection; it does not run Git or an SSH RPC itself.
Checkpoint: `6fbeb2abda6e987f459e452942aedbcec937ecb7`.
This adds no timer, TTL, settled-result cache, Git command, SSH RPC method, relay method, runtime
transport, or renderer store owner. Existing status/upstream APIs remain fresh after settlement.
## Subscription boundary
Each subscription is exact for:
- native versus exact WSL distro or the current `SshGitProvider` instance;
- registered worktree/folder backing path and local shared-link paths;
- status options including `includeIgnored`, negative-cache bypass, and `reuseLineStats`;
- configured upstream versus every explicit `GitPushTarget` field, including absent
`remoteCreated` versus explicit `false`; and
- the subscribing `WebContents`.
IPC events contain only the caller-generated subscription ID, state, generation, revision, and SSH
provider incarnation. Repository paths and snapshots are never broadcast. Explicit unsubscribe,
sender destruction/reload, context or visibility change, connection loss, provider replacement,
and handler disposal all detach the exact owner listener.
Checks derives its local identity from the resolved project runtime, so changing a project-level
Windows-host/WSL override or WSL distro changes the context and rebinds the subscription.
SSH registry replacement increments `incarnation`, detaches the old provider before rebinding, and
starts the replacement provider at generation/revision zero. A binding token suppresses late
events from the old provider. Active runtime environments return `null` from the desktop
subscription client and keep the existing runtime/SSH visibility interval and fresh RPC route.
The owner emits `invalidated` at existing mutation fences and `ready` only when the requested
status plus configured or explicit upstream projection is admissible. Retention-truncated status
and ambiguous ahead-plus-behind upstream state do not wake Checks. A retained explicit upstream
can complete a later status revision only when HEAD and branch identity are unchanged.
An ambiguous newer embedded configured-upstream projection cannot fall through to an older
complete projection.
Checks subscribes to the ordinary and `reuseLineStats: true` polling identities. Its revision gate:
1. advances invalidation/provider-incarnation fences without launching work;
2. coalesces duplicate same-revision delivery;
3. records ready events arriving during a Checks read;
4. queries the owner again after a fresh fallback without running Git; and
5. schedules a trailing read only when the newest ready revision is newer than the projection the
completed read observed.
Monotonic read tokens fence late completion from a previous context. Subscription handles are
retained as each registration settles, so context cleanup is exact even if its sibling registration
remains pending. A renderer send failure closes the main-process subscription and cannot rebind a
replacement SSH provider or reject the status/upstream producer.
This removes `gitStatusByWorktree` and `remoteStatusesByWorktree` from the automatic Checks
snapshot effect dependencies. Those store values remain only as the existing bounded Publish
fallback, while manual Refresh, retry, runtime routing, and cold/inadmissible fallback behavior are
unchanged.
## Deterministic operation counts
The retained owner test overlaps two identical status callers and two identical explicit-upstream
callers, then has the ready listener query the projection.
| Boundary | A | B |
| --------------------------------------------------------------------- | --: | --: |
| Physical local/WSL status reads | 1 | 1 |
| Physical local/WSL explicit-upstream reads | 1 | 1 |
| SSH `git.status` RPCs under the retained settled-overlap path | 1 | 1 |
| SSH `git.upstreamStatus` RPCs under the retained settled-overlap path | 1 | 1 |
| Physical work launched by one ready event | 0 | 0 |
| Automatic Checks store-trigger dependencies | 2 | 0 |
| Exact owner projection queries after a fresh fallback | 0 | 1 |
The extra B query is memory-only. It observes the publication revision so the ready event produced
by the same fallback does not launch duplicate status/upstream work. Existing ten-caller status
and upstream owner suites continue to prove 10-to-1 physical sharing and unchanged exact commands
and RPC payloads.
For fan-out, 100 same-identity owner subscribers receive 100 callbacks backed by one frozen owner
event object. After 100 unsubscriptions the owner retains zero listeners. Checks normally owns two
preload listeners, one per exact polling identity; both are removed on cleanup. Main allocates one
small incarnation wrapper per targeted subscription because different SSH incarnations cannot
share that object.
## Production A/B
Fresh builds were archived outside the worktree:
- A: `/tmp/orca-phase2-snapshot-revision-subscription-a.h4Db8W/out`
- retained B: `/tmp/orca-phase2-snapshot-revision-subscription-reviewed.sd8qpB/out`
| Artifact | A raw / gzip | B raw / gzip | Delta | A SHA-256 | B SHA-256 |
| ---------------------- | ----------------- | ----------------- | --------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Electron main entry | 789,869 / 176,858 | 795,061 / 177,868 | +5,192 / +1,010 | `7f4984a54c65cf5b749a87ed7ec7f2074c5b844992215ad25e8c17d179045660` | `e536ed0c16ca8071e74fb1bfdf9253ea8c75b2e1cf9cb40987f6f78085de5f7e` |
| Electron preload entry | 130,891 / 20,658 | 131,910 / 20,835 | +1,019 / +177 | `afa8494e803f0e57cabdb5d2ff636f30fc8021069a7694d57d2ecdcdd6837a8a` | `2bdcea2978045dcc5ac328c8cc4a32ab2c49da83fcd82ebaa2c8f69f9532c0fc` |
| Renderer manifest | 403,000 / 48,288 | 403,037 / 48,306 | +37 / +18 | `9f57d550a1c9f6c75bf2d3db4f5ff53e53c4c52eceb8b10e9b86d8659a48d7fe` | `b615db1461347955088d93a91186d2b07997b761eb2e9a21c50433261914f731` |
Electron main remains 30,048 raw bytes below its unchanged 825,109 budget. Preload remains 90 raw
bytes below its unchanged 132,000 budget.
| Renderer entry | A raw / gzip | B raw / gzip | Delta | A JS/CSS | B JS/CSS |
| -------------- | --------------------- | --------------------- | ------ | -------- | -------- |
| Index | 8,416,540 / 1,877,824 | 8,416,540 / 1,877,828 | 0 / +4 | 292 / 2 | 292 / 2 |
| Popout | 4,507,253 / 984,615 | 4,507,253 / 984,615 | 0 / 0 | 77 / 2 | 77 / 2 |
| Web | 4,360,652 / 928,352 | 4,360,652 / 928,351 | 0 / -1 | 33 / 1 | 33 / 1 |
| Tree | A files / SHA-256 | B files / SHA-256 |
| -------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Main | 184 / `fb9d10e85030515e08c78e1226fa574c3c3828a68b2639b4a0cf0773b185fd11` | 184 / `d30fbdb2eedf419d28f4aeda85f1df22877329621c12c951b2c4412d155328c8` |
| Preload | 1 / `750483cc40dc0e5f9612ea63f2e09d587ef77230ceda36144ee1a380c0274908` | 1 / `c220e1f7cda9e772c28b894048aae889c81bd7167811f10df9a3cd601cf250ef` |
| Renderer | 787 / `3e37f385175e2b51309723d5b5bf50742d9256bb11399f1b3913122e87850acb` | 787 / `422a591173e891ae1de38a9014ece3ef2342131b2e5636466baf4a608fab2a02` |
Complete A and B manifest validation found 778 records, three HTML entries, 213 dynamic edges,
860 emitted references, no missing or escaping targets, no cross-entry imports, and no static
cycles. A has 6,247 static edges and B has 6,248 because the Checks chunk now imports its concrete
revision hook.
## Independent review
Independent acceptance added regression coverage and corrected:
- ambiguous embedded configured-upstream admission through an older projection;
- a late read from a previous context consuming the current context's revision;
- sender delivery failure escaping into a producer or leaking an SSH rebind;
- a successful subscription handle being hidden from cleanup by a pending sibling; and
- project-level Windows/WSL runtime changes not rebinding the exact host identity.
The core IPC registrar test was also updated to exercise the new store-bound registrar.
## Validation
- Focused owner, IPC, preload, provider registry, desktop routing, status projection, revision gate,
and hook tests: 75 passed.
- Broad host status/cache, native/WSL, SSH provider/dispatch, filesystem, IPC, and preload suite:
376 passed, one skipped.
- Broad renderer snapshot, polling, refresh scheduler, folder-workspace, runtime routing, and Checks
suite: 121 passed.
- Core IPC registrar suite: two passed.
- `pnpm run typecheck:node`: passed.
- `pnpm run typecheck:web`: passed.
- Targeted `pnpm exec oxlint --deny-warnings`: passed.
- Targeted `pnpm exec oxfmt --check`: passed.
- `pnpm run check:max-lines-ratchet`: passed with 354 grandfathered suppressions and no new bypass.
- Fresh A and retained B `pnpm run build:electron-vite`: passed.
- `pnpm run check:electron-bundle-budgets`: passed against retained B.
- Complete renderer manifest/path/static-cycle validation: passed for A and B.
- `git diff --check`: passed.
- Added/changed max-lines-disable scan: zero.
The production build emitted the two pre-existing CSS optimizer warnings for the standard
`::highlight(markdown-preview-search-*)` selectors; no new build warning was introduced.
## Limitations and next seam
- Subscription savings require active polling to publish an admissible status/upstream projection.
Cold, failed, stale, truncated, branch-mismatched, runtime-routed, and manual-refresh paths retain
the fresh fallback.
- Invalidation alone deliberately does not wake Checks. The matching ready event follows the
active poller's complete status/upstream publication, preventing a status-only wakeup from
launching duplicate upstream work.
- Measurements are deterministic mocked owner/RPC operation counts, not live Git latency across
every supported host.
- Runtime/mobile snapshot transport and renderer snapshot payload subscriptions remain later work.
The next seam is migrating another desktop status consumer to the revision/query boundary, then
introducing a bounded provider-neutral transport only when a runtime consumer is selected.
- No packaged smoke was run on Windows, WSL, Linux, or a live SSH host. Existing path, identity,
provider replacement, mutation fence, command, and routing suites cover the unchanged behavior.