Merge remote-tracking branch 'origin/feat/directory-picker' into feat/workspace-directory-browser
This commit is contained in:
@@ -29,13 +29,21 @@ export function apply(ctx: ClientContext): void {
|
||||
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.
|
||||
// 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)))
|
||||
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)))
|
||||
ctx.slots.register({ name: 'sidebar.workspaces.directoryFlow', inject: injected }, NativeDirectoryFlow), lateConflict))
|
||||
} catch (error) {
|
||||
for (const entry of deferred) entry.dispose()
|
||||
throw error
|
||||
|
||||
@@ -56,6 +56,47 @@ describe('directory-picker-native client half', () => {
|
||||
for (const hole of HOLES) expect(after.slots.entries(hole)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('rolls back wholesale and reports loudly when a rival provider wins after deferred activation', async () => {
|
||||
const b = await bench()
|
||||
const rejections: unknown[] = []
|
||||
const onUnhandled = (reason: unknown): void => { rejections.push(reason) }
|
||||
// queueMicrotask throws surface as uncaughtException, not a rejection.
|
||||
process.on('unhandledRejection', onUnhandled)
|
||||
process.on('uncaughtException', onUnhandled)
|
||||
try {
|
||||
// This provider activates BEFORE any hole exists: both deferrals wait.
|
||||
// (Duplicate rows of the SAME package converge silently — the deferral
|
||||
// skips a hole its own component already occupies; the conflict needs
|
||||
// a rival provider.)
|
||||
await b.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
b.declare()
|
||||
// A rival occupies both holes ahead of the pending microtask flush.
|
||||
b.slots.register({ name: HOLES[0] } as never, () => null)
|
||||
b.slots.register({ name: HOLES[1] } as never, () => null)
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
// The rival keeps both holes; this provider rolled back wholesale and
|
||||
// surfaced the conflict on the fail-loud channel — no partial mix.
|
||||
for (const hole of HOLES) expect(b.slots.entries(hole)).toHaveLength(1)
|
||||
expect(rejections.map(String).join('\n')).toContain('already has a registration')
|
||||
|
||||
// Non-Error conflicts wrap before the loud rethrow (same channel).
|
||||
const c = await bench()
|
||||
await c.ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const original = c.slots.register.bind(c.slots)
|
||||
const slotsAny = c.slots as { register: typeof original }
|
||||
slotsAny.register = ((options: never, component: never) => {
|
||||
if ((options as { name?: string }).name === HOLES[0]) throw 'string conflict'
|
||||
return original(options, component)
|
||||
}) as typeof original
|
||||
c.declare()
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(rejections.map(String).join('\n')).toContain('string conflict')
|
||||
} finally {
|
||||
process.off('unhandledRejection', onUnhandled)
|
||||
process.off('uncaughtException', onUnhandled)
|
||||
}
|
||||
})
|
||||
|
||||
it('rolls back the first deferral when the second hole is already occupied', async () => {
|
||||
const b = await bench()
|
||||
b.declare()
|
||||
|
||||
Reference in New Issue
Block a user