mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(client): deferGroupRegistration owns the multi-hole flow semantics
The construction-rollback + late-conflict-rollback + loud-rethrow block was about to be a verbatim clone across the two flow packages; ui-slots now owns it as deferGroupRegistration (one occupant, several holes, as a unit), with direct specs for all three arms, and the native flow consumes it.
This commit is contained in:
@@ -89,3 +89,40 @@ export function deferRegistration(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer ONE occupant into several holes as a unit. Construction that throws
|
||||
* partway (a declared hole already occupied registers synchronously) rolls
|
||||
* every earlier deferral back before rethrowing; a failure surfacing from a
|
||||
* LATER ledger flush (holes declared after rival providers activated) rolls
|
||||
* the whole group back the same way and re-raises the wrapped error on the
|
||||
* global channel the boot's fail-loud handler owns — never a throw through
|
||||
* the slot flush, never partial occupancy from the group's owner.
|
||||
* @param registry - the slot registry face.
|
||||
* @param names - the target holes (one registration per name).
|
||||
* @param component - the occupant whose ledger presence marks "registered".
|
||||
* @param register - performs one hole's registration; returns its disposer.
|
||||
* @returns the group handle (dispose in the owning effect's disposer).
|
||||
* @throws the immediate registration's failure, after rolling the group back.
|
||||
*/
|
||||
export function deferGroupRegistration<K extends string>(
|
||||
registry: DeferralRegistry,
|
||||
names: readonly K[],
|
||||
component: unknown,
|
||||
register: (name: K) => () => void,
|
||||
): { dispose: () => void } {
|
||||
const deferred: DeferredRegistration[] = []
|
||||
const lateFailure = (error: unknown): void => {
|
||||
for (const entry of deferred) entry.dispose()
|
||||
queueMicrotask(() => { throw error instanceof Error ? error : new Error(String(error)) })
|
||||
}
|
||||
try {
|
||||
for (const name of names) {
|
||||
deferred.push(deferRegistration(registry, name, component, () => register(name), lateFailure))
|
||||
}
|
||||
} catch (error) {
|
||||
for (const entry of deferred) entry.dispose()
|
||||
throw error
|
||||
}
|
||||
return { dispose: () => { for (const entry of deferred) entry.dispose() } }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// re-registration, and — the failure contract — no subscription survives a
|
||||
// construction that throws synchronously (an already-occupied single slot).
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { deferRegistration, SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { deferGroupRegistration, deferRegistration, SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
|
||||
// Shares the merges declared by core.spec.ts (same program); reuse its keys.
|
||||
const HOLE = 'test.single' as const
|
||||
@@ -63,3 +63,64 @@ describe('deferRegistration', () => {
|
||||
expect(core.entries(HOLE)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deferGroupRegistration', () => {
|
||||
const HOLES = ['test.single', 'test.grandchild'] as const
|
||||
|
||||
function declaredPair(): SlotCore {
|
||||
const core = new SlotCore()
|
||||
core.register({
|
||||
name: 'root',
|
||||
children: Object.fromEntries(HOLES.map(name => [name, { kind: 'single', scope: 'root' }])),
|
||||
} as never, (() => null) as never)
|
||||
return core
|
||||
}
|
||||
|
||||
it('registers the whole group and disposes it as a unit', () => {
|
||||
const core = declaredPair()
|
||||
const component = (): null => null
|
||||
const group = deferGroupRegistration(core, HOLES, component, name =>
|
||||
core.register({ name } as never, component as never))
|
||||
for (const name of HOLES) expect(core.entries(name)).toHaveLength(1)
|
||||
group.dispose()
|
||||
for (const name of HOLES) expect(core.entries(name)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('rolls the group back when construction fails partway', () => {
|
||||
const core = declaredPair()
|
||||
const component = (): null => null
|
||||
core.register({ name: HOLES[1] } as never, (() => null) as never)
|
||||
expect(() => deferGroupRegistration(core, HOLES, component, name =>
|
||||
core.register({ name } as never, component as never))).toThrow(/already has a registration/)
|
||||
// The first hole's registration and subscription rolled back with it.
|
||||
expect(core.entries(HOLES[0])).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('rolls the group back and re-raises loudly on a late conflict', async () => {
|
||||
const core = new SlotCore()
|
||||
const component = (): null => null
|
||||
const failures: unknown[] = []
|
||||
const onLoud = (reason: unknown): void => { failures.push(reason) }
|
||||
process.on('uncaughtException', onLoud)
|
||||
try {
|
||||
const group = deferGroupRegistration(core, HOLES, component, name =>
|
||||
core.register({ name } as never, component as never))
|
||||
// Declaration lands with a rival racing in ahead of the flush.
|
||||
core.register({
|
||||
name: 'root',
|
||||
children: Object.fromEntries(HOLES.map(name => [name, { kind: 'single', scope: 'root' }])),
|
||||
} as never, (() => null) as never)
|
||||
core.register({ name: HOLES[0] } as never, (() => null) as never)
|
||||
core.register({ name: HOLES[1] } as never, (() => null) as never)
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(failures.map(String).join('')).toContain('already has a registration')
|
||||
// No partial occupancy from the group's owner survives.
|
||||
for (const name of HOLES) {
|
||||
expect(core.entries(name).filter(entry => entry.component === component)).toHaveLength(0)
|
||||
}
|
||||
group.dispose()
|
||||
} finally {
|
||||
process.off('uncaughtException', onLoud)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* both sides of the native interaction with one cordis.yml row; no client
|
||||
* code branches on a capability kind.
|
||||
*/
|
||||
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { deferGroupRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
// Type-only: pulls the SlotMap merge declaring the directory-flow holes.
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
||||
@@ -27,27 +27,15 @@ export const inject = ['slots', 'workspaces']
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const injected = (): NativeFlowInjected => ({ pick: () => ctx.workspaces.pickDirectory() })
|
||||
ctx.effect(() => {
|
||||
// Constructing the pair can throw halfway (a declared hole already
|
||||
// occupied registers synchronously): roll the earlier deferral back so
|
||||
// no live subscription outlives the failed fiber. A conflict surfacing
|
||||
// from a LATER ledger flush (holes declared after two flow providers
|
||||
// activated) rolls the whole pair back the same way and re-raises on
|
||||
// the global channel the boot's fail-loud handler owns — never a throw
|
||||
// through the slot flush, never partial occupancy from this package.
|
||||
const deferred: ReturnType<typeof deferRegistration>[] = []
|
||||
const lateConflict = (error: unknown): void => {
|
||||
for (const entry of deferred) entry.dispose()
|
||||
queueMicrotask(() => { throw error instanceof Error ? error : new Error(String(error)) })
|
||||
}
|
||||
try {
|
||||
deferred.push(deferRegistration(ctx.slots, 'conversation.hero.workspace.directoryFlow', NativeDirectoryFlow, () =>
|
||||
ctx.slots.register({ name: 'conversation.hero.workspace.directoryFlow', inject: injected }, NativeDirectoryFlow), lateConflict))
|
||||
deferred.push(deferRegistration(ctx.slots, 'sidebar.workspaces.directoryFlow', NativeDirectoryFlow, () =>
|
||||
ctx.slots.register({ name: 'sidebar.workspaces.directoryFlow', inject: injected }, NativeDirectoryFlow), lateConflict))
|
||||
} catch (error) {
|
||||
for (const entry of deferred) entry.dispose()
|
||||
throw error
|
||||
}
|
||||
return () => { for (const entry of deferred) entry.dispose() }
|
||||
// One occupant, both holes, as a unit: construction or late conflicts
|
||||
// (holes declared after rival providers activated) roll the whole pair
|
||||
// back and fail loud — semantics owned by deferGroupRegistration.
|
||||
const group = deferGroupRegistration(
|
||||
ctx.slots,
|
||||
['conversation.hero.workspace.directoryFlow', 'sidebar.workspaces.directoryFlow'] as const,
|
||||
NativeDirectoryFlow,
|
||||
name => ctx.slots.register({ name, inject: injected }, NativeDirectoryFlow),
|
||||
)
|
||||
return () => { group.dispose() }
|
||||
}, 'directory-picker-native: flow registrations')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user